From be8a4d3c8d9f2eb6334719f2b85d808004d73837 Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Thu, 18 Jun 2026 14:30:19 +0000 Subject: [PATCH 01/75] perf: [DSM-144] Misc certification optimizations * Use precomputed empty leaf hash instead of computing it from scratch every time. * Pass the key to `MapTransformFork::mk_tree` by reference, don't clone it (especially since it's only used in a couple of cases). * Simplidy CanisterFork. --- .../src/lazy_tree_conversion.rs | 84 ++++++++++--------- rs/canonical_state/tree_hash/src/hash_tree.rs | 31 ++++++- rs/types/wasm_types/src/lib.rs | 5 ++ 3 files changed, 76 insertions(+), 44 deletions(-) diff --git a/rs/canonical_state/src/lazy_tree_conversion.rs b/rs/canonical_state/src/lazy_tree_conversion.rs index add6d8cf22b1..b87cf5e1c120 100644 --- a/rs/canonical_state/src/lazy_tree_conversion.rs +++ b/rs/canonical_state/src/lazy_tree_conversion.rs @@ -229,7 +229,7 @@ impl MapFilter for NoFilter { #[derive(Clone)] struct MapTransformFork<'a, K, V, MF, F> where - F: Fn(K, &'a V, CertificationVersion) -> LazyTree<'a>, + F: Fn(&'a K, &'a V, CertificationVersion) -> LazyTree<'a>, MF: MapFilter, { map: &'a BTreeMap, @@ -243,7 +243,7 @@ where K: Ord + LabelLike + Clone + Send + Sync, V: Send + Sync, MF: MapFilter + Send + Sync, - F: Fn(K, &'a V, CertificationVersion) -> LazyTree<'a> + Send + Sync, + F: Fn(&'a K, &'a V, CertificationVersion) -> LazyTree<'a> + Send + Sync, { fn edge(&self, label: &Label) -> Option> { let k = K::from_label(label.as_bytes())?; @@ -251,8 +251,8 @@ where return None; } self.map - .get(&k) - .map(move |v| (self.mk_tree)(k, v, self.certification_version)) + .get_key_value(&k) + .map(|(k, v)| (self.mk_tree)(k, v, self.certification_version)) } fn labels(&self) -> Box + '_> { @@ -272,7 +272,7 @@ where .map(move |(k, v)| { ( k.to_label(), - (self.mk_tree)(k.clone(), v, self.certification_version), + (self.mk_tree)(k, v, self.certification_version), ) }), ) @@ -771,7 +771,6 @@ fn status_to_tree(status: &IngressStatus) -> LazyTree<'_> { } const CERTIFIED_DATA_LABEL: &[u8] = b"certified_data"; -const CONTROLLER_LABEL: &[u8] = b"controller"; const CONTROLLERS_LABEL: &[u8] = b"controllers"; const METADATA_LABEL: &[u8] = b"metadata"; const MODULE_HASH_LABEL: &[u8] = b"module_hash"; @@ -792,60 +791,65 @@ struct CanisterFork<'a> { } impl<'a> CanisterFork<'a> { - /// Like `edge`, but skips the version check on every call. - fn edge_no_checks(&self, label: &[u8]) -> Option> { + /// Like `edge`, but assumes valid labels only. + fn edge_no_checks(&self, label: &[u8]) -> LazyTree<'a> { let canister = self.canister; match canister.execution_state.as_ref() { Some(execution_state) => match label { - CERTIFIED_DATA_LABEL => Some(Blob(&canister.system_state.certified_data[..], None)), - CONTROLLER_LABEL => Some(Blob(canister.system_state.controller().as_slice(), None)), - CONTROLLERS_LABEL => Some(blob(move || { - encode_controllers(&canister.system_state.controllers) - })), - METADATA_LABEL => Some(canister_metadata_as_tree(execution_state, self.version)), - MODULE_HASH_LABEL => Some(blob(move || { - execution_state.wasm_binary.binary.module_hash().to_vec() - })), - _ => None, + CERTIFIED_DATA_LABEL => { + Blob(&canister.system_state.certified_data.as_slice(), None) + } + CONTROLLERS_LABEL => { + blob(move || encode_controllers(&canister.system_state.controllers)) + } + METADATA_LABEL => canister_metadata_as_tree(execution_state, self.version), + MODULE_HASH_LABEL => { + Blob(execution_state.wasm_binary.binary.module_hash_ref(), None) + } + _ => unreachable!(), }, None => match label { - CONTROLLER_LABEL => Some(Blob(canister.system_state.controller().as_slice(), None)), - CONTROLLERS_LABEL => Some(blob(move || { - encode_controllers(&canister.system_state.controllers) - })), - _ => None, + CONTROLLERS_LABEL => { + blob(move || encode_controllers(&canister.system_state.controllers)) + } + _ => unreachable!(), }, } } + + /// Returns the labels applicable to this canister. + #[inline] + fn valid_labels(&self) -> &'static [&'static [u8]] { + match self.canister.execution_state { + Some(_) => &CANISTER_LABELS, + None => &CANISTER_NO_MODULE_LABELS, + } + } } impl<'a> LazyFork<'a> for CanisterFork<'a> { fn edge(&self, label: &Label) -> Option> { - CANISTER_LABELS.iter().find(|l| *l == &label.as_bytes())?; - self.edge_no_checks(label.as_bytes()) + self.valid_labels() + .iter() + .find(|l| *l == &label.as_bytes())?; + Some(self.edge_no_checks(label.as_bytes())) } fn labels(&self) -> Box + 'a> { - match self.canister.execution_state { - Some(_) => Box::new(CANISTER_LABELS.iter().map(From::from)), - None => Box::new(CANISTER_NO_MODULE_LABELS.iter().map(From::from)), - } + Box::new(self.valid_labels().iter().map(From::from)) } fn children(&self) -> Box)> + 'a> { let canister = self.clone(); Box::new( - CANISTER_LABELS.iter().filter_map(move |label| { - Some((Label::from(label), canister.edge_no_checks(label)?)) - }), + self.valid_labels() + .iter() + .map(move |label| (Label::from(label), canister.edge_no_checks(label))), ) } fn len(&self) -> usize { - match self.canister.execution_state { - Some(_) => CANISTER_LABELS.len(), - None => CANISTER_NO_MODULE_LABELS.len(), - } + self.valid_labels().len() } } @@ -942,17 +946,15 @@ fn subnets_as_tree<'a>( blob({ let inverted_routing_table = Arc::clone(&inverted_routing_table); move || { - encode_subnet_canister_ranges( - inverted_routing_table.get(&subnet_id), - ) + encode_subnet_canister_ranges(inverted_routing_table.get(subnet_id)) } }), ) - .with_if(subnet_id == own_subnet_id, "node", move || { + .with_if(subnet_id == &own_subnet_id, "node", move || { nodes_as_tree(own_subnet_node_public_keys, certification_version) }) .with_tree_if( - subnet_id == own_subnet_id, + subnet_id == &own_subnet_id, "metrics", blob(move || encode_subnet_metrics(metrics, certification_version)), ) diff --git a/rs/canonical_state/tree_hash/src/hash_tree.rs b/rs/canonical_state/tree_hash/src/hash_tree.rs index c3eee0a2dbfb..a21459427a9b 100644 --- a/rs/canonical_state/tree_hash/src/hash_tree.rs +++ b/rs/canonical_state/tree_hash/src/hash_tree.rs @@ -22,6 +22,13 @@ const EMPTY_HASH: Digest = Digest([ 0xcf, 0xfb, 0x6c, 0x47, 0xdb, 0xab, 0x21, 0x6e, 0x79, 0x30, 0xe8, 0x2f, 0x81, 0x90, 0xd1, 0x20, ]); +/// Hash of an empty leaf, i.e. the digest of the domain separator +/// "ic-hashtree-leaf" with no body bytes. +const EMPTY_LEAF_HASH: Digest = Digest([ + 0xd0, 0x01, 0xf3, 0xe7, 0xb8, 0x21, 0x66, 0xc6, 0xd3, 0x43, 0xa1, 0xef, 0xe7, 0x76, 0xe9, 0x6a, + 0xc0, 0x2a, 0x23, 0xa5, 0x1e, 0x08, 0x98, 0xbc, 0x2c, 0x4e, 0x32, 0x3f, 0xce, 0x0e, 0x62, 0x2c, +]); + /// 30 LSBs are used to store the index const INDEX_MASK: u32 = 0x3fff_ffff; /// 2 MSBs are used to store the node kind @@ -820,6 +827,7 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { } match t { + LazyTree::Blob(b, None) if b.is_empty() => ht.new_leaf(EMPTY_LEAF_HASH), LazyTree::Blob(b, None) => { let mut h = Hasher::for_domain("ic-hashtree-leaf"); h.update(b); @@ -836,12 +844,20 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { } LazyTree::LazyBlob(f) => { let b = f(); + if b.is_empty() { + return ht.new_leaf(EMPTY_LEAF_HASH); + } + let mut h = Hasher::for_domain("ic-hashtree-leaf"); h.update(&b); ht.new_leaf(h.finalize()) } LazyTree::LazyFork(f) => { let num_children = f.len(); + if num_children == 0 { + return Ok(NodeId::empty()); + } + let NodeIndexRange { bucket, index_range: range, @@ -884,9 +900,7 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { } } - if nodes.is_empty() { - return Ok(NodeId::empty()); - } else if nodes.len() == 1 { + if nodes.len() == 1 { return Ok(nodes[0]); } @@ -1011,3 +1025,14 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { Ok(ht) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_leaf_hash_matches_hasher() { + let h = Hasher::for_domain("ic-hashtree-leaf"); + assert_eq!(h.finalize(), EMPTY_LEAF_HASH); + } +} diff --git a/rs/types/wasm_types/src/lib.rs b/rs/types/wasm_types/src/lib.rs index c2cedbfc7a3a..2da471461937 100644 --- a/rs/types/wasm_types/src/lib.rs +++ b/rs/types/wasm_types/src/lib.rs @@ -136,6 +136,11 @@ impl CanisterModule { self.module_hash } + /// Returns the Sha256 hash of this Wasm module. + pub fn module_hash_ref(&self) -> &[u8; WASM_HASH_LENGTH] { + &self.module_hash + } + /// Returns the loading status of the module storage. pub fn module_loading_status(&self) -> ModuleLoadingStatus { match &self.module { From 76df8ed51cdabd72e555caa69f97609233040d57 Mon Sep 17 00:00:00 2001 From: Alin Sinpalean <58422065+alin-at-dfinity@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:32:42 +0200 Subject: [PATCH 02/75] Fix pattern match for empty Blob in hash_tree --- rs/canonical_state/tree_hash/src/hash_tree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/canonical_state/tree_hash/src/hash_tree.rs b/rs/canonical_state/tree_hash/src/hash_tree.rs index a21459427a9b..888812eb38c3 100644 --- a/rs/canonical_state/tree_hash/src/hash_tree.rs +++ b/rs/canonical_state/tree_hash/src/hash_tree.rs @@ -827,7 +827,7 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { } match t { - LazyTree::Blob(b, None) if b.is_empty() => ht.new_leaf(EMPTY_LEAF_HASH), + LazyTree::Blob([], None) => ht.new_leaf(EMPTY_LEAF_HASH), LazyTree::Blob(b, None) => { let mut h = Hasher::for_domain("ic-hashtree-leaf"); h.update(b); From a6f274fd6f0b82c97aa95a1b70096b26e17d945b Mon Sep 17 00:00:00 2001 From: Alin Sinpalean <58422065+alin-at-dfinity@users.noreply.github.com> Date: Sat, 20 Jun 2026 00:26:59 +0200 Subject: [PATCH 03/75] Make clippy happy. --- rs/canonical_state/src/lazy_tree_conversion.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/canonical_state/src/lazy_tree_conversion.rs b/rs/canonical_state/src/lazy_tree_conversion.rs index b87cf5e1c120..f72a724bcf31 100644 --- a/rs/canonical_state/src/lazy_tree_conversion.rs +++ b/rs/canonical_state/src/lazy_tree_conversion.rs @@ -797,7 +797,7 @@ impl<'a> CanisterFork<'a> { match canister.execution_state.as_ref() { Some(execution_state) => match label { CERTIFIED_DATA_LABEL => { - Blob(&canister.system_state.certified_data.as_slice(), None) + Blob(canister.system_state.certified_data.as_slice(), None) } CONTROLLERS_LABEL => { blob(move || encode_controllers(&canister.system_state.controllers)) @@ -979,7 +979,7 @@ fn canister_ranges_as_tree( map_filter: NoFilter, certification_version, mk_tree: move |subnet_id, _subnet_topology, _certification_version| { - let split_ranges = split_routing_table.get(&subnet_id).map(Arc::clone); + let split_ranges = split_routing_table.get(subnet_id).map(Arc::clone); fork(CanisterRangesFork { split_ranges, phantom: PhantomData, From 793c4a5c2a6c5139e973c40cfce57b41511caef2 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Fri, 19 Jun 2026 22:32:30 +0000 Subject: [PATCH 04/75] Automatically fixing code for linting and formatting issues --- rs/canonical_state/src/lazy_tree_conversion.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rs/canonical_state/src/lazy_tree_conversion.rs b/rs/canonical_state/src/lazy_tree_conversion.rs index f72a724bcf31..aa85cf097c3d 100644 --- a/rs/canonical_state/src/lazy_tree_conversion.rs +++ b/rs/canonical_state/src/lazy_tree_conversion.rs @@ -796,9 +796,7 @@ impl<'a> CanisterFork<'a> { let canister = self.canister; match canister.execution_state.as_ref() { Some(execution_state) => match label { - CERTIFIED_DATA_LABEL => { - Blob(canister.system_state.certified_data.as_slice(), None) - } + CERTIFIED_DATA_LABEL => Blob(canister.system_state.certified_data.as_slice(), None), CONTROLLERS_LABEL => { blob(move || encode_controllers(&canister.system_state.controllers)) } From 8dada3ee029423f55b626a79037da2b70f3d4068 Mon Sep 17 00:00:00 2001 From: Alin Sinpalean <58422065+alin-at-dfinity@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:10:27 +0200 Subject: [PATCH 05/75] fix: Also reduce read_ahead for dm-* devices (#10420) Looks like it was actually dm-2 (store-crypt) and/or dm-4 (store-shared--data, more likely) suffering from read amplification, not vda. They're probably all the same (virtual?) device underneath, which is why reads appear to be spiking on all 3 at once. But it's likely the shared data partition that is actually being pounded. Reduce the read-ahead for all these devices similar to what was done for vda, just to be safe. --- ic-os/components/guestos/misc/sysfs.d/read_ahead.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/ic-os/components/guestos/misc/sysfs.d/read_ahead.conf b/ic-os/components/guestos/misc/sysfs.d/read_ahead.conf index da4a4a0a4de6..d07d1794bd98 100644 --- a/ic-os/components/guestos/misc/sysfs.d/read_ahead.conf +++ b/ic-os/components/guestos/misc/sysfs.d/read_ahead.conf @@ -1,4 +1,5 @@ # Block device read_ahead configuration # +block/dm-*/queue/read_ahead_kb = 128 block/vda/queue/read_ahead_kb = 128 From 24ad019c33f920739bf0feb0f0fb4cee28c708d4 Mon Sep 17 00:00:00 2001 From: pietrodimarco-dfinity <124565147+pietrodimarco-dfinity@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:18:16 +0200 Subject: [PATCH 06/75] fix: honor --json for ic-admin get-elected-{guestos,hostos}-versions (#10422) ## Summary `ic-admin get-elected-guestos-versions --json` (alias `get-blessed-replica-versions`) stopped emitting JSON. The handler used to print JSON via the generic `print_and_get_last_value` helper, but #10301 rewrote it to enumerate `ReplicaVersionRecord` entries with a plain `println!` loop and never re-wired `opts.json`. Since `--json` is a global flag, it is still accepted but silently ignored, so the command always prints plaintext. `get-elected-hostos-versions` had the same plaintext-only behavior. This broke downstream automation in `dre-airflow`, which parsed the JSON output and started failing with `JSONDecodeError: Extra data: line 1 column 7 (char 6)` (the first version hash's leading digits parsed as an integer). This PR restores `--json` for both subcommands: - with `--json`: print the elected version IDs as a JSON array (`["", ...]`) - without `--json`: unchanged, newline-separated version IDs ## How was this tested? `bazel build //rs/registry/admin:ic-admin` builds successfully. Manual shape (with `--json`): ```json [ "abc...def", "012...789" ] ``` --------- Co-authored-by: IDX GitHub Automation --- rs/registry/admin/bin/main.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/rs/registry/admin/bin/main.rs b/rs/registry/admin/bin/main.rs index 9640fc438830..ca93e30ef416 100644 --- a/rs/registry/admin/bin/main.rs +++ b/rs/registry/admin/bin/main.rs @@ -4905,9 +4905,16 @@ async fn main() { let guestos_versions = registry_client .get_all_replica_version_records(registry_client.get_latest_version()) - .unwrap(); + .unwrap() + .unwrap_or_default(); - if let Some(guestos_versions) = guestos_versions { + if opts.json { + let version_ids: Vec = guestos_versions + .into_iter() + .map(|(version, _)| version) + .collect(); + println!("{}", serde_json::to_string_pretty(&version_ids).unwrap()); + } else { for (version, _) in guestos_versions { println!("{}", version); } @@ -5861,9 +5868,16 @@ async fn main() { let hostos_versions = registry_client .get_hostos_versions(registry_client.get_latest_version()) - .unwrap(); + .unwrap() + .unwrap_or_default(); - if let Some(hostos_versions) = hostos_versions { + if opts.json { + let version_ids: Vec = hostos_versions + .into_iter() + .map(|version| version.hostos_version_id) + .collect(); + println!("{}", serde_json::to_string_pretty(&version_ids).unwrap()); + } else { for version in hostos_versions { println!("{}", version.hostos_version_id); } From 5b3d56286253aae42496003bfa98893b234829f4 Mon Sep 17 00:00:00 2001 From: "pr-creation-bot-dfinity-ic[bot]" <200595415+pr-creation-bot-dfinity-ic[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:48:40 +0000 Subject: [PATCH 07/75] chore: Update Mainnet ICOS revisions file (#10421) Update mainnet revisions file to include the latest version released on the mainnet. This PR is created automatically using [`mainnet_revisions.py`](https://github.com/dfinity/ic/blob/master/ci/src/mainnet_revisions/mainnet_revisions.py) Co-authored-by: CI Automation --- mainnet-icos-revisions.json | 90 +++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/mainnet-icos-revisions.json b/mainnet-icos-revisions.json index fdf0749ae86b..d4ca6ff5d13b 100644 --- a/mainnet-icos-revisions.json +++ b/mainnet-icos-revisions.json @@ -123,45 +123,45 @@ } }, "io67a-2jmkw-zup3h-snbwi-g6a5n-rm5dn-b6png-lvdpl-nqnto-yih6l-gqe": { - "version": "a47e5434753752c1d2972fbc4407d14f88964285", - "update_img_hash": "390f4b6c7e3788192be38b970fe62fa12677943a6e281944f4b8a58ec4f461e6", - "update_img_hash_dev": "0ed6a2c0be8b95591ee9fb1d70a1105a3f5e5874942128002b4a4e2393fd8f72", + "version": "fb721da900b9e9219773ee312f987971338f7c62", + "update_img_hash": "3e6cb724f0cc0a17d1692e91e1f95cfc883c4f6e49146cd57921e69617cedabc", + "update_img_hash_dev": "2ee8c2809aadef08645c453c0388afbb53db410e47665d66a7f214af3334d4e6", "launch_measurements": { "guest_launch_measurements": [ { - "measurement": [ 107, 32, 139, 57, 115, 42, 23, 108, 94, 228, 237, 173, 203, 68, 176, 7, 93, 142, 200, 168, 127, 50, 229, 156, 118, 48, 13, 18, 195, 30, 223, 93, 99, 173, 65, 126, 133, 205, 55, 108, 87, 127, 253, 90, 15, 104, 246, 92 ], + "measurement": [ 18, 227, 13, 20, 22, 234, 253, 96, 194, 4, 236, 231, 88, 29, 125, 41, 18, 235, 183, 80, 143, 222, 243, 72, 27, 183, 170, 66, 233, 17, 228, 16, 243, 245, 27, 238, 113, 52, 139, 136, 225, 121, 209, 248, 53, 67, 198, 219 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 134, 243, 30, 195, 220, 82, 75, 231, 255, 24, 201, 98, 205, 64, 154, 40, 45, 149, 223, 178, 134, 53, 198, 139, 142, 9, 61, 98, 111, 164, 201, 197, 252, 157, 16, 109, 250, 190, 98, 254, 144, 121, 168, 0, 154, 228, 85, 228 ], + "measurement": [ 168, 20, 35, 37, 80, 160, 194, 73, 121, 117, 175, 87, 158, 43, 227, 86, 23, 93, 118, 104, 10, 43, 72, 8, 251, 95, 53, 210, 107, 57, 159, 101, 251, 163, 160, 61, 240, 8, 82, 78, 93, 109, 19, 238, 10, 147, 125, 128 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 36, 4, 216, 25, 43, 212, 130, 146, 197, 113, 24, 15, 244, 180, 157, 242, 124, 167, 17, 132, 137, 22, 59, 23, 44, 28, 205, 188, 132, 37, 22, 194, 203, 155, 175, 57, 142, 254, 14, 204, 210, 255, 53, 199, 235, 64, 228, 3 ], + "measurement": [ 246, 185, 48, 139, 248, 209, 51, 77, 211, 115, 215, 117, 123, 115, 148, 85, 62, 80, 126, 125, 231, 190, 189, 43, 202, 76, 117, 228, 17, 216, 164, 127, 98, 197, 166, 229, 95, 100, 23, 3, 155, 136, 114, 79, 147, 237, 61, 140 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 22, 183, 43, 124, 65, 130, 159, 27, 133, 75, 133, 10, 188, 53, 113, 208, 212, 186, 107, 152, 152, 223, 114, 143, 23, 36, 63, 195, 209, 133, 250, 68, 83, 156, 36, 232, 18, 83, 48, 200, 49, 251, 37, 8, 75, 239, 153, 48 ], + "measurement": [ 140, 175, 182, 40, 232, 142, 53, 109, 105, 198, 36, 254, 12, 229, 156, 206, 37, 54, 180, 212, 177, 103, 188, 232, 5, 192, 187, 63, 54, 71, 185, 162, 40, 157, 245, 126, 92, 228, 179, 98, 80, 209, 212, 163, 4, 68, 170, 72 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 85, 212, 49, 185, 51, 65, 178, 112, 182, 222, 32, 234, 195, 54, 72, 39, 44, 122, 68, 245, 10, 161, 31, 183, 159, 65, 6, 178, 35, 92, 240, 88, 92, 145, 104, 47, 207, 247, 36, 67, 111, 121, 126, 16, 204, 67, 238, 79 ], + "measurement": [ 127, 58, 219, 103, 155, 159, 173, 5, 157, 169, 196, 5, 3, 123, 239, 211, 19, 224, 219, 31, 211, 124, 22, 206, 210, 232, 7, 66, 155, 60, 10, 47, 175, 183, 125, 165, 202, 58, 51, 113, 147, 128, 84, 165, 83, 152, 197, 26 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 14, 156, 102, 144, 105, 52, 183, 48, 70, 15, 52, 72, 237, 148, 202, 125, 61, 224, 177, 196, 18, 59, 149, 240, 34, 123, 13, 70, 16, 28, 83, 1, 70, 56, 72, 17, 45, 198, 108, 200, 248, 157, 239, 87, 181, 245, 233, 42 ], + "measurement": [ 210, 100, 7, 43, 164, 116, 27, 36, 179, 36, 71, 190, 75, 116, 20, 196, 19, 143, 23, 45, 144, 105, 69, 99, 154, 222, 226, 77, 29, 137, 40, 136, 56, 164, 49, 179, 4, 205, 95, 126, 79, 94, 206, 246, 75, 215, 175, 103 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } } ] @@ -169,75 +169,87 @@ "launch_measurements_dev": { "guest_launch_measurements": [ { - "measurement": [ 75, 75, 167, 109, 233, 98, 201, 171, 9, 233, 185, 47, 23, 162, 73, 46, 63, 149, 103, 126, 193, 21, 145, 54, 181, 129, 72, 51, 131, 93, 197, 190, 143, 130, 132, 242, 177, 183, 16, 86, 119, 122, 184, 48, 135, 200, 186, 50 ], + "measurement": [ 85, 172, 241, 226, 57, 162, 121, 60, 236, 234, 186, 245, 106, 55, 155, 5, 18, 59, 67, 128, 45, 108, 59, 235, 157, 88, 65, 159, 197, 246, 199, 87, 197, 177, 172, 199, 130, 130, 97, 213, 183, 14, 78, 255, 28, 93, 37, 6 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 140, 203, 244, 143, 191, 127, 2, 59, 147, 52, 61, 185, 97, 79, 105, 54, 126, 6, 243, 178, 105, 150, 91, 185, 44, 131, 130, 138, 38, 184, 203, 226, 52, 195, 176, 18, 107, 156, 253, 74, 67, 186, 91, 52, 231, 228, 161, 184 ], + "measurement": [ 167, 190, 44, 164, 33, 11, 140, 155, 83, 189, 161, 253, 157, 69, 71, 100, 245, 16, 21, 9, 116, 142, 112, 80, 205, 152, 90, 248, 194, 135, 190, 123, 191, 135, 240, 208, 183, 188, 120, 168, 180, 210, 131, 47, 149, 71, 222, 90 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 69, 64, 9, 189, 59, 38, 171, 246, 145, 67, 77, 216, 61, 50, 102, 72, 86, 173, 200, 101, 196, 224, 179, 68, 245, 127, 58, 200, 211, 131, 173, 255, 195, 39, 106, 116, 210, 231, 252, 23, 122, 240, 249, 70, 147, 253, 201, 146 ], + "measurement": [ 10, 164, 78, 132, 174, 241, 130, 149, 222, 144, 49, 226, 67, 139, 196, 31, 52, 225, 206, 88, 230, 54, 166, 49, 204, 131, 171, 127, 193, 145, 239, 118, 249, 82, 128, 199, 169, 173, 147, 144, 232, 102, 235, 22, 91, 248, 30, 216 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 31, 113, 210, 32, 92, 227, 224, 247, 145, 123, 20, 15, 53, 235, 40, 235, 8, 190, 124, 211, 70, 27, 68, 181, 222, 113, 235, 213, 83, 101, 208, 142, 216, 188, 72, 68, 118, 80, 205, 44, 153, 52, 129, 154, 93, 130, 220, 70 ], + "measurement": [ 40, 118, 28, 168, 70, 224, 64, 214, 100, 4, 162, 87, 97, 171, 65, 173, 58, 154, 24, 105, 60, 133, 206, 37, 99, 236, 156, 223, 254, 55, 207, 172, 246, 54, 61, 136, 233, 83, 187, 136, 206, 251, 158, 43, 103, 127, 119, 114 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 138, 227, 7, 224, 83, 105, 220, 249, 176, 61, 56, 213, 27, 203, 76, 3, 9, 123, 141, 32, 2, 18, 2, 105, 18, 114, 134, 34, 220, 109, 12, 108, 238, 136, 32, 201, 156, 173, 89, 174, 112, 85, 149, 183, 52, 230, 168, 118 ], + "measurement": [ 126, 219, 193, 137, 242, 209, 198, 170, 251, 179, 170, 55, 84, 99, 45, 69, 28, 109, 239, 20, 132, 86, 175, 43, 161, 44, 37, 246, 50, 160, 212, 202, 242, 157, 250, 244, 146, 44, 16, 170, 110, 83, 69, 129, 139, 81, 116, 30 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 71, 150, 67, 183, 239, 4, 174, 223, 28, 99, 239, 86, 227, 38, 17, 198, 74, 125, 63, 2, 30, 37, 132, 74, 246, 255, 175, 252, 227, 154, 153, 149, 188, 93, 247, 253, 4, 226, 118, 152, 36, 31, 86, 75, 214, 51, 192, 26 ], + "measurement": [ 104, 145, 174, 38, 17, 214, 8, 132, 155, 229, 229, 13, 161, 54, 139, 62, 49, 239, 169, 126, 30, 240, 87, 122, 241, 178, 146, 87, 127, 46, 4, 79, 108, 160, 247, 99, 143, 217, 183, 17, 42, 114, 250, 71, 108, 55, 66, 142 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 14, 51, 47, 86, 244, 91, 201, 89, 152, 73, 46, 198, 106, 13, 238, 85, 48, 228, 229, 180, 66, 99, 241, 164, 204, 30, 49, 82, 234, 160, 193, 155, 118, 137, 151, 110, 119, 226, 64, 56, 94, 136, 135, 54, 232, 51, 12, 253 ], + "measurement": [ 48, 163, 104, 111, 165, 233, 157, 124, 140, 98, 49, 182, 156, 2, 244, 135, 52, 121, 42, 68, 43, 165, 80, 46, 239, 191, 62, 233, 131, 139, 141, 15, 216, 153, 165, 63, 67, 141, 166, 156, 162, 85, 71, 169, 235, 25, 242, 54 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 14, 68, 147, 205, 183, 240, 11, 231, 148, 203, 80, 250, 27, 38, 164, 148, 117, 247, 17, 106, 22, 87, 185, 227, 49, 185, 82, 73, 58, 235, 247, 185, 196, 200, 236, 175, 97, 84, 252, 133, 109, 96, 159, 105, 250, 124, 221, 104 ], + "measurement": [ 95, 93, 149, 160, 171, 205, 254, 68, 115, 167, 69, 26, 159, 71, 175, 4, 73, 36, 58, 29, 11, 215, 190, 201, 66, 197, 170, 174, 77, 177, 85, 235, 191, 71, 194, 151, 190, 211, 164, 49, 183, 138, 152, 239, 123, 87, 83, 240 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 45, 62, 43, 118, 128, 89, 119, 239, 71, 22, 184, 55, 95, 223, 2, 159, 191, 104, 125, 105, 11, 133, 32, 168, 1, 199, 124, 173, 31, 221, 96, 69, 197, 125, 107, 223, 200, 80, 184, 147, 246, 0, 75, 87, 80, 86, 93, 131 ], + "measurement": [ 110, 231, 68, 215, 217, 134, 191, 3, 2, 212, 186, 124, 24, 18, 38, 113, 170, 63, 164, 248, 181, 185, 255, 202, 109, 108, 164, 142, 68, 230, 157, 105, 89, 20, 177, 68, 5, 88, 241, 216, 254, 119, 12, 204, 57, 25, 73, 60 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 42, 139, 145, 0, 203, 82, 212, 146, 64, 141, 162, 164, 126, 1, 80, 80, 249, 252, 83, 189, 79, 77, 201, 196, 69, 199, 56, 213, 77, 60, 72, 236, 56, 83, 220, 168, 130, 37, 251, 15, 164, 233, 1, 105, 200, 110, 156, 139 ], + "measurement": [ 74, 29, 216, 211, 158, 76, 145, 152, 190, 95, 195, 122, 71, 95, 179, 198, 48, 107, 99, 254, 103, 45, 44, 202, 78, 207, 210, 161, 169, 219, 73, 189, 160, 51, 179, 102, 82, 34, 150, 148, 40, 217, 87, 150, 185, 237, 217, 222 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 54, 9, 241, 90, 119, 204, 65, 103, 174, 254, 156, 223, 207, 157, 140, 128, 80, 26, 154, 133, 187, 178, 14, 111, 87, 60, 236, 236, 74, 161, 78, 231, 103, 66, 37, 207, 247, 255, 133, 42, 58, 227, 85, 36, 242, 233, 193, 210 ], + "measurement": [ 62, 218, 131, 254, 38, 89, 53, 214, 14, 33, 42, 222, 124, 44, 190, 126, 26, 155, 214, 243, 77, 106, 226, 43, 64, 50, 203, 58, 201, 176, 253, 77, 100, 0, 47, 179, 104, 35, 226, 157, 93, 29, 150, 15, 7, 7, 58, 226 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 125, 185, 91, 212, 61, 140, 115, 121, 203, 9, 166, 228, 145, 220, 236, 12, 122, 163, 238, 219, 23, 102, 195, 6, 229, 189, 13, 225, 39, 135, 203, 246, 216, 187, 43, 53, 132, 183, 5, 149, 243, 253, 226, 42, 119, 79, 170, 192 ], + "measurement": [ 125, 85, 104, 138, 75, 24, 98, 129, 194, 247, 104, 201, 243, 7, 105, 179, 25, 144, 172, 83, 123, 101, 38, 245, 54, 40, 237, 22, 8, 181, 158, 125, 136, 100, 104, 157, 68, 121, 87, 244, 6, 31, 209, 32, 197, 76, 61, 3 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "vcpu_type": "EPYC-Turin" } } ] From ec4f6f6e533764b0e09158695e496500ca769f62 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:33:10 +0200 Subject: [PATCH 08/75] feat: subnet deletion in PocketIC (#10407) This PR adds a PocketIC operation to delete a subnet: - the subnet's `StateMachine` is properly dropped from the PocketIC state (including state directory removal); - the registry is updated accordingly (including registry changes in the registry canister if the registry canister has been bootstrapped by PocketIC, i.e., not manually). Note. This PR does not implement subnet deletion in DSM! Stale artifacts (such as stale streams and hanging unbounded-wait calls) might persist. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/pocket-ic/CHANGELOG.md | 1 + packages/pocket-ic/src/common/rest.rs | 91 +++++++-- packages/pocket-ic/src/lib.rs | 7 + packages/pocket-ic/src/nonblocking.rs | 13 ++ .../pocket-ic/test_canister/src/canister.rs | 8 + packages/pocket-ic/tests/tests.rs | 182 ++++++++++++++++++ rs/pocket_ic_server/CHANGELOG.md | 4 + rs/pocket_ic_server/src/pocket_ic.rs | 169 ++++++++++++++-- rs/pocket_ic_server/src/state_api/routes.rs | 25 ++- rs/state_machine_tests/src/lib.rs | 40 ++-- 10 files changed, 491 insertions(+), 49 deletions(-) diff --git a/packages/pocket-ic/CHANGELOG.md b/packages/pocket-ic/CHANGELOG.md index 925450645c9f..ec77d7ae69d1 100644 --- a/packages/pocket-ic/CHANGELOG.md +++ b/packages/pocket-ic/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added the `PATCH` variant to the `CanisterHttpMethod` enum (canister HTTPS outcalls). Note: `PATCH` outcalls are currently rejected by the execution layer until support has rolled out to all replicas. +- The function `PocketIc::delete_subnet` to delete a subnet. Only non-named subnets (application, cloud engine, system, or verified application) can be deleted. ## 14.0.0 - 2026-05-26 diff --git a/packages/pocket-ic/src/common/rest.rs b/packages/pocket-ic/src/common/rest.rs index 61b58cb7db53..ab9d2c12a7ec 100644 --- a/packages/pocket-ic/src/common/rest.rs +++ b/packages/pocket-ic/src/common/rest.rs @@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::net::SocketAddr; use std::path::PathBuf; +use strum::IntoEnumIterator; use strum_macros::EnumIter; pub type InstanceId = usize; @@ -482,6 +483,58 @@ pub enum SubnetKind { VerifiedApplication, } +impl SubnetKind { + pub fn is_named(self) -> bool { + NamedSubnet::try_from(self).is_ok() + } +} + +/// Named subnets have a fixed canister ID range on the IC mainnet and at most one +/// instance per PocketIC instance. +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, EnumIter)] +pub enum NamedSubnet { + NNS, + SNS, + II, + Fiduciary, + Bitcoin, + TestThresholdKeys, +} + +impl From for SubnetKind { + fn from(named: NamedSubnet) -> Self { + match named { + NamedSubnet::NNS => SubnetKind::NNS, + NamedSubnet::SNS => SubnetKind::SNS, + NamedSubnet::II => SubnetKind::II, + NamedSubnet::Fiduciary => SubnetKind::Fiduciary, + NamedSubnet::Bitcoin => SubnetKind::Bitcoin, + NamedSubnet::TestThresholdKeys => SubnetKind::TestThresholdKeys, + } + } +} + +/// The exhaustive match over `SubnetKind` enforces structural consistency: adding a new +/// `SubnetKind` variant is a compile error until it is explicitly placed in either the named +/// or unnamed arm here and in `From for SubnetKind`. +impl TryFrom for NamedSubnet { + type Error = (); + fn try_from(kind: SubnetKind) -> Result { + match kind { + SubnetKind::NNS => Ok(NamedSubnet::NNS), + SubnetKind::SNS => Ok(NamedSubnet::SNS), + SubnetKind::II => Ok(NamedSubnet::II), + SubnetKind::Fiduciary => Ok(NamedSubnet::Fiduciary), + SubnetKind::Bitcoin => Ok(NamedSubnet::Bitcoin), + SubnetKind::TestThresholdKeys => Ok(NamedSubnet::TestThresholdKeys), + SubnetKind::Application + | SubnetKind::CloudEngine + | SubnetKind::System + | SubnetKind::VerifiedApplication => Err(()), + } + } +} + /// This represents which named subnets the user wants to create, and how /// many of the general app/system subnets, which are indistinguishable. #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, JsonSchema)] @@ -805,25 +858,31 @@ impl SubnetStateConfig { } impl ExtendedSubnetConfigSet { + fn named_subnet_spec(&self, named: NamedSubnet) -> Option { + match named { + NamedSubnet::NNS => self.nns.clone(), + NamedSubnet::SNS => self.sns.clone(), + NamedSubnet::II => self.ii.clone(), + NamedSubnet::Fiduciary => self.fiduciary.clone(), + NamedSubnet::Bitcoin => self.bitcoin.clone(), + NamedSubnet::TestThresholdKeys => self.test_threshold_keys.clone(), + } + } + // Return the configured named subnets in order. #[allow(clippy::type_complexity)] pub fn get_named(&self) -> Vec<(SubnetKind, Option, SubnetInstructionConfig)> { - use SubnetKind::*; - vec![ - (self.nns.clone(), NNS), - (self.sns.clone(), SNS), - (self.ii.clone(), II), - (self.fiduciary.clone(), Fiduciary), - (self.bitcoin.clone(), Bitcoin), - (self.test_threshold_keys.clone(), TestThresholdKeys), - ] - .into_iter() - .filter(|(mb, _)| mb.is_some()) - .map(|(mb, kind)| { - let spec = mb.unwrap(); - (kind, spec.get_state_path(), spec.get_instruction_config()) - }) - .collect() + NamedSubnet::iter() + .filter_map(|named| { + self.named_subnet_spec(named).map(|spec| { + ( + SubnetKind::from(named), + spec.get_state_path(), + spec.get_instruction_config(), + ) + }) + }) + .collect() } pub fn validate(&self) -> Result<(), String> { diff --git a/packages/pocket-ic/src/lib.rs b/packages/pocket-ic/src/lib.rs index 135d23663639..c6df68e61152 100644 --- a/packages/pocket-ic/src/lib.rs +++ b/packages/pocket-ic/src/lib.rs @@ -1480,6 +1480,13 @@ impl PocketIc { runtime.block_on(async { self.pocket_ic.canister_exists(canister_id).await }) } + /// Deletes a subnet. Panics if the subnet does not exist or is a named subnet. + #[instrument(ret, skip(self), fields(instance_id=self.pocket_ic.instance_id, subnet_id = %subnet_id.to_string()))] + pub fn delete_subnet(&self, subnet_id: SubnetId) { + let runtime = self.runtime.clone(); + runtime.block_on(async { self.pocket_ic.delete_subnet(subnet_id).await }) + } + /// Returns the subnet ID of the canister if the canister exists. #[instrument(ret, skip(self), fields(instance_id=self.pocket_ic.instance_id, canister_id = %canister_id.to_string()))] pub fn get_subnet(&self, canister_id: CanisterId) -> Option { diff --git a/packages/pocket-ic/src/nonblocking.rs b/packages/pocket-ic/src/nonblocking.rs index 73b8166e8de9..d6a87bd63253 100644 --- a/packages/pocket-ic/src/nonblocking.rs +++ b/packages/pocket-ic/src/nonblocking.rs @@ -1600,6 +1600,19 @@ impl PocketIc { self.get_subnet(canister_id).await.is_some() } + /// Deletes a subnet. Panics if the subnet does not exist or is a named subnet. + #[instrument(ret, skip(self), fields(instance_id=self.instance_id, subnet_id = %subnet_id.to_string()))] + pub async fn delete_subnet(&self, subnet_id: SubnetId) { + let endpoint = "update/delete_subnet"; + self.post::<(), RawSubnetId>( + endpoint, + RawSubnetId { + subnet_id: subnet_id.as_slice().to_vec(), + }, + ) + .await; + } + /// Returns the subnet ID of the canister if the canister exists. #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))] pub async fn get_subnet(&self, canister_id: CanisterId) -> Option { diff --git a/packages/pocket-ic/test_canister/src/canister.rs b/packages/pocket-ic/test_canister/src/canister.rs index a50a59c01355..dac63bf92a66 100644 --- a/packages/pocket-ic/test_canister/src/canister.rs +++ b/packages/pocket-ic/test_canister/src/canister.rs @@ -398,6 +398,14 @@ async fn whois(canister: Principal) -> String { .0 } +#[update] +async fn call_and_get_rejection_code(canister: Principal) -> u32 { + match ic_cdk::call::<_, (String,)>(canister, "whoami", ((),)).await { + Ok(_) => 0, + Err((code, _)) => code as u32, + } +} + #[update] async fn blob_len(blob: Vec) -> usize { blob.len() diff --git a/packages/pocket-ic/tests/tests.rs b/packages/pocket-ic/tests/tests.rs index 8440f8102416..bcf55d5d5410 100644 --- a/packages/pocket-ic/tests/tests.rs +++ b/packages/pocket-ic/tests/tests.rs @@ -3631,3 +3631,185 @@ fn cloud_engine_default_effective_canister_id() { topology.default_effective_canister_id.clone().into(); assert_eq!(effective_canister_id, default_effective_canister_id); } + +#[test] +fn test_delete_subnet() { + // Create a PocketIC instance with two application subnets. + let pic = PocketIcBuilder::new() + .with_application_subnet() + .with_application_subnet() + .build(); + + let subnet_id_1 = pic.topology().get_app_subnets()[0]; + let subnet_id_2 = pic.topology().get_app_subnets()[1]; + assert_ne!(subnet_id_1, subnet_id_2); + + // Deploy test canisters on both subnets. + let canister_1 = pic.create_canister_on_subnet(None, None, subnet_id_1); + pic.add_cycles(canister_1, INIT_CYCLES); + pic.install_canister(canister_1, test_canister_wasm(), vec![], None); + + let canister_2 = pic.create_canister_on_subnet(None, None, subnet_id_2); + pic.add_cycles(canister_2, INIT_CYCLES); + pic.install_canister(canister_2, test_canister_wasm(), vec![], None); + + // (1) Verify that ingress message and inter-canister call to canister_2 work. + let reply = pic + .update_call( + canister_2, + Principal::anonymous(), + "whoami", + encode_one(()).unwrap(), + ) + .expect("ingress call to canister_2 failed before subnet deletion"); + assert_eq!(Decode!(&reply, String).unwrap(), canister_2.to_string()); + + let reply = pic + .update_call( + canister_1, + Principal::anonymous(), + "call_and_get_rejection_code", + Encode!(&canister_2).unwrap(), + ) + .expect("inter-canister call via canister_1 failed before subnet deletion"); + assert_eq!(Decode!(&reply, u32).unwrap(), 0); + + // (2) Delete subnet_2. + pic.delete_subnet(subnet_id_2); + + // (3) Verify that ingress message to canister_2 fails after subnet deletion. + // The server rejects the message with a 4xx HTTP status (BadIngressMessage), causing the client to panic. + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + pic.update_call( + canister_2, + Principal::anonymous(), + "whoami", + encode_one(()).unwrap(), + ) + })); + assert!( + result.is_err(), + "ingress call to canister_2 should fail after subnet deletion" + ); + + // (4) Verify that inter-canister call to canister_2 fails with DestinationInvalid (3). + let reply = pic + .update_call( + canister_1, + Principal::anonymous(), + "call_and_get_rejection_code", + Encode!(&canister_2).unwrap(), + ) + .expect("inter-canister call via canister_1 should succeed"); + assert_eq!( + Decode!(&reply, u32).unwrap(), + RejectCode::DestinationInvalid as u32 + ); +} + +#[test] +fn test_delete_default_effective_canister_id_subnet_fails() { + // Create a PocketIC instance with one NNS subnet and two application subnets. + let pic = PocketIcBuilder::new() + .with_nns_subnet() + .with_application_subnet() + .with_application_subnet() + .build(); + + let topology = pic.topology(); + let default_effective_canister_id = + Principal::from(topology.default_effective_canister_id.clone()); + + // Identify which app subnet contains the default effective canister ID. + let default_subnet_id = topology + .get_subnet(default_effective_canister_id) + .expect("default effective canister ID must belong to a subnet"); + let other_subnet_id = topology + .get_app_subnets() + .into_iter() + .find(|&id| id != default_subnet_id) + .expect("there must be a second app subnet"); + + // The app subnet containing the default effective canister ID cannot be deleted. + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + pic.delete_subnet(default_subnet_id); + })); + assert!( + result.is_err(), + "deleting the subnet containing the default effective canister ID should fail" + ); + + // The other app subnet can be deleted. + pic.delete_subnet(other_subnet_id); + assert_eq!(pic.topology().get_app_subnets(), vec![default_subnet_id]); +} + +#[test] +fn test_delete_named_subnet_fails() { + let pic = PocketIcBuilder::new() + .with_nns_subnet() + .with_fiduciary_subnet() + .build(); + + let nns_subnet_id = pic.topology().get_nns().unwrap(); + let fiduciary_subnet_id = pic.topology().get_fiduciary().unwrap(); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + pic.delete_subnet(nns_subnet_id); + })); + assert!(result.is_err(), "deleting NNS subnet should fail"); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + pic.delete_subnet(fiduciary_subnet_id); + })); + assert!(result.is_err(), "deleting fiduciary subnet should fail"); +} + +#[test] +fn test_delete_root_app_subnet_fails() { + // Create a PocketIC instance with a single application subnet. + // The first subnet created becomes the root subnet and cannot be deleted. + let pic = PocketIcBuilder::new().with_application_subnet().build(); + + let app_subnet_id = pic.topology().get_app_subnets()[0]; + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + pic.delete_subnet(app_subnet_id); + })); + assert!(result.is_err(), "deleting the root app subnet should fail"); +} + +#[test] +fn test_delete_subnet_state_dir() { + let state_dir = TempDir::new().unwrap(); + let state_dir_path = state_dir.path().to_path_buf(); + + let pic = PocketIcBuilder::new() + .with_state_dir(state_dir_path.clone()) + .with_application_subnet() + .with_application_subnet() + .build(); + + let subnet_dirs_count = || { + std::fs::read_dir(&state_dir_path) + .unwrap() + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_dir()) + .count() + }; + + let subnet_ids = pic.topology().get_app_subnets(); + assert_eq!(subnet_ids.len(), 2); + // On Windows, the state_dir is only synced back from the WSL-native state directory on drop. + #[cfg(not(windows))] + assert_eq!(subnet_dirs_count(), 2); + + pic.delete_subnet(subnet_ids[1]); + assert_eq!(pic.topology().get_app_subnets(), vec![subnet_ids[0]]); + + // Drop to flush state to disk. + drop(pic); + + // After deletion, only the remaining subnet's state directory should exist. + assert_eq!(subnet_dirs_count(), 1); +} diff --git a/rs/pocket_ic_server/CHANGELOG.md b/rs/pocket_ic_server/CHANGELOG.md index 9d8a3a484bc3..5583b13c121f 100644 --- a/rs/pocket_ic_server/CHANGELOG.md +++ b/rs/pocket_ic_server/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- The endpoint `/instances//update/delete_subnet` to delete a subnet (non-named subnets only: application, cloud engine, system, or verified application subnets). + If a state directory is configured for the instance, the deleted subnet's state directory is removed from disk. + ## 14.0.0 - 2026-05-26 ### Added diff --git a/rs/pocket_ic_server/src/pocket_ic.rs b/rs/pocket_ic_server/src/pocket_ic.rs index 9ac5c13bc466..eb838c4b0baf 100644 --- a/rs/pocket_ic_server/src/pocket_ic.rs +++ b/rs/pocket_ic_server/src/pocket_ic.rs @@ -110,7 +110,7 @@ use ic_sns_wasm::pb::v1::{AddWasmRequest, AddWasmResponse, SnsCanisterType, SnsW use ic_state_machine_tests::{ FakeVerifier, StateMachine, StateMachineBuilder, StateMachineConfig, StateMachineStateDir, SubmitIngressError, Subnets, WasmResult, add_global_registry_records, - add_initial_registry_records, + add_initial_registry_records, update_global_registry_records, }; use ic_state_manager::StateManagerImpl; use ic_types::batch::BlockmakerMetrics; @@ -533,6 +533,9 @@ impl SubnetsImpl { pub(crate) fn get_all(&self) -> Vec> { self.subnets.read().unwrap().values().cloned().collect() } + fn remove(&self, subnet_id: SubnetId) -> Option> { + self.subnets.write().unwrap().remove(&subnet_id) + } fn clear(&self) { self.subnets.write().unwrap().clear(); } @@ -2740,6 +2743,109 @@ impl PocketIcSubnets { subnet.state_machine.reload_registry(); } } + + fn delete_subnet( + &mut self, + subnet_id: SubnetId, + default_effective_canister_id: Principal, + ) -> Result<(), PocketIcError> { + let config_pos = self + .subnet_configs + .iter() + .position(|c| c.subnet_id == subnet_id) + .ok_or(PocketIcError::SubnetNotFound(subnet_id.get().0))?; + + let subnet_kind = self.subnet_configs[config_pos].subnet_kind; + if subnet_kind.is_named() { + return Err(PocketIcError::Forbidden(format!( + "Cannot delete named subnet {} (kind: {:?}).", + subnet_id, subnet_kind + ))); + } + + if let Some(root_subnet) = self.nns_subnet.as_ref() + && root_subnet.get_subnet_id() == subnet_id + { + return Err(PocketIcError::Forbidden( + "Cannot delete the root subnet of the PocketIC instance.".to_string(), + )); + } + + let default_canister_id: CanisterId = PrincipalId(default_effective_canister_id) + .try_into() + .unwrap(); + // The default effective canister ID is used as the routing target for canister + // creation calls that don't specify an explicit subnet (provisional API with ic_00 + // as effective ID, or no effective principal). Deleting its subnet would break + // all such calls. + if let Some((_, default_subnet_id)) = self.routing_table.lookup_entry(default_canister_id) + && default_subnet_id == subnet_id + { + return Err(PocketIcError::Forbidden(format!( + "Cannot delete subnet {} which contains the default effective canister ID.", + subnet_id + ))); + } + + let config = self.subnet_configs.remove(config_pos); + + let subnet = self + .subnets + .remove(subnet_id) + .expect("subnet in subnet_configs must be in subnets"); + + subnet.state_machine.drop_payload_builder(); + + self.routing_table.remove_subnet(subnet_id); + + for subnets in self.chain_keys.values_mut() { + subnets.retain(|&sid| sid != subnet_id); + } + self.chain_keys.retain(|_, subnets| !subnets.is_empty()); + + // Delete the subnet state directory from disk. + if let Some(state_dir) = self.state_dir.get() { + let subnet_seed = compute_subnet_seed(config.ranges.clone(), config.alloc_range); + let subnet_state_dir = state_dir.join(hex::encode(subnet_seed)); + if subnet_state_dir.exists() + && let Err(e) = std::fs::remove_dir_all(&subnet_state_dir) + { + eprintln!( + "Failed to delete subnet state directory {}: {}", + subnet_state_dir.display(), + e + ); + } + } + + // Update global registry records to reflect the removed subnet. + if self.nns_subnet.is_some() { + let next_version = + RegistryVersion::new(self.registry_data_provider.latest_version().get() + 1); + let subnet_list = self + .subnets + .get_all() + .into_iter() + .map(|s| s.get_subnet_id()) + .collect(); + update_global_registry_records( + next_version, + self.routing_table.clone(), + subnet_list, + self.chain_keys.clone(), + self.registry_data_provider.clone(), + ); + self.persist_registry_changes(); + } + + // Drop the StateMachine, waiting until no other Arc holders remain. + let state_machine = subnet.state_machine.clone(); + drop(subnet); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5 * 60); + drop_state_machine(state_machine, deadline); + + Ok(()) + } } #[async_trait] @@ -2776,6 +2882,25 @@ pub struct PocketIc { default_effective_canister_id: Principal, } +fn drop_state_machine(state_machine: Arc, deadline: std::time::Instant) { + let mut state_machine = Some(state_machine); + loop { + match Arc::try_unwrap(state_machine.take().unwrap()) { + Ok(sm) => { + sm.drop(); + break; + } + Err(sm) => { + state_machine = Some(sm); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + } + if std::time::Instant::now() > deadline { + panic!("Timed out while dropping StateMachine."); + } + } +} + impl Drop for PocketIc { fn drop(&mut self) { if self.subnets.state_dir.get().is_some() { @@ -2800,24 +2925,10 @@ impl Drop for PocketIc { .collect(); self.subnets.clear(); // for every StateMachine, wait until nobody else has an Arc to that StateMachine - // and then drop that StateMachine - let start = std::time::Instant::now(); + // and then drop that StateMachine; the deadline is shared across all StateMachines + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5 * 60); for state_machine in state_machines { - let mut state_machine = Some(state_machine); - while state_machine.is_some() { - match Arc::try_unwrap(state_machine.take().unwrap()) { - Ok(sm) => { - sm.drop(); - break; - } - Err(sm) => { - state_machine = Some(sm); - } - } - if start.elapsed() > std::time::Duration::from_secs(5 * 60) { - panic!("Timed out while dropping PocketIC."); - } - } + drop_state_machine(state_machine, deadline); } } } @@ -5460,6 +5571,28 @@ impl Operation for GetSubnet { } } +#[derive(Clone, Debug)] +pub struct DeleteSubnet { + pub subnet_id: SubnetId, +} + +impl Operation for DeleteSubnet { + fn compute(&self, pic: &mut PocketIc) -> OpOut { + let default_effective_canister_id = pic.default_effective_canister_id; + match pic + .subnets + .delete_subnet(self.subnet_id, default_effective_canister_id) + { + Ok(()) => OpOut::NoOutput, + Err(e) => OpOut::Error(e), + } + } + + fn id(&self) -> OpId { + OpId(format!("delete_subnet({})", self.subnet_id)) + } +} + /// Add cycles to a given canister. /// /// # Panics diff --git a/rs/pocket_ic_server/src/state_api/routes.rs b/rs/pocket_ic_server/src/state_api/routes.rs index 36f05389e38d..16c02b59dcde 100644 --- a/rs/pocket_ic_server/src/state_api/routes.rs +++ b/rs/pocket_ic_server/src/state_api/routes.rs @@ -10,10 +10,11 @@ use super::state::{ }; use crate::pocket_ic::{ AddCycles, AwaitIngressMessage, CallRequest, CallRequestVersion, CanisterReadStateRequest, - CanisterSnapshotDownload, CanisterSnapshotUpload, DashboardRequest, GetCanisterHttp, - GetControllers, GetCyclesBalance, GetStableMemory, GetSubnet, GetTime, GetTopology, - IngressMessageStatus, MockCanisterHttp, PubKey, Query, QueryRequest, SetCertifiedTime, - SetStableMemory, SetTime, StatusRequest, SubmitIngressMessage, SubnetReadStateRequest, Tick, + CanisterSnapshotDownload, CanisterSnapshotUpload, DashboardRequest, DeleteSubnet, + GetCanisterHttp, GetControllers, GetCyclesBalance, GetStableMemory, GetSubnet, GetTime, + GetTopology, IngressMessageStatus, MockCanisterHttp, PubKey, Query, QueryRequest, + SetCertifiedTime, SetStableMemory, SetTime, StatusRequest, SubmitIngressMessage, + SubnetReadStateRequest, Tick, }; use crate::{ BlobStore, InstanceId, OpId, Operation, SubnetBlockmakers, async_trait, pocket_ic::PocketIc, @@ -146,6 +147,7 @@ where "/canister_snapshot_upload", post(handler_canister_snapshot_upload), ) + .directory_route("/delete_subnet", post(handler_delete_subnet)) } async fn handle_limit_error(req: Request, next: Next) -> Response { @@ -810,6 +812,21 @@ pub async fn handler_pub_key( (code, Json(res)) } +pub async fn handler_delete_subnet( + State(AppState { api_state, .. }): State, + Path(instance_id): Path, + headers: HeaderMap, + extract::Json(RawSubnetId { subnet_id }): extract::Json, +) -> (StatusCode, Json>) { + let timeout = timeout_or_default(headers); + let subnet_id = ic_types::SubnetId::new(ic_types::PrincipalId(candid::Principal::from_slice( + &subnet_id, + ))); + let op = DeleteSubnet { subnet_id }; + let (code, res) = run_operation(api_state, instance_id, timeout, op).await; + (code, Json(res)) +} + pub async fn handler_canister_snapshot_download( State(AppState { api_state, .. }): State, headers: HeaderMap, diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 0c44533a495a..050953d5c6b9 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -270,6 +270,35 @@ pub fn add_global_registry_records( ) .unwrap(); + update_global_registry_records( + registry_version, + routing_table, + subnet_list, + chain_keys, + registry_data_provider.clone(), + ); + + // node rewards table + registry_data_provider + .add( + NODE_REWARDS_TABLE_KEY, + registry_version, + Some(NodeRewardsTable { + table: BTreeMap::new(), + }), + ) + .unwrap(); +} + +/// Updates global registry records (routing table, subnet list, chain keys) at the given +/// registry version. Used both during initial setup and after removing a subnet. +pub fn update_global_registry_records( + registry_version: RegistryVersion, + routing_table: RoutingTable, + subnet_list: Vec, + chain_keys: BTreeMap>, + registry_data_provider: Arc, +) { // routing table record let pb_routing_table = PbRoutingTable::from(routing_table.clone()); registry_data_provider @@ -301,17 +330,6 @@ pub fn add_global_registry_records( ) .unwrap(); } - - // node rewards table - registry_data_provider - .add( - NODE_REWARDS_TABLE_KEY, - registry_version, - Some(NodeRewardsTable { - table: BTreeMap::new(), - }), - ) - .unwrap(); } /// Adds initial registry records to the registry managed by the registry data provider: From 1ff18ee3c6b11f63482915a95b4fcdb6d21aac0e Mon Sep 17 00:00:00 2001 From: Alin Sinpalean <58422065+alin-at-dfinity@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:57:33 +0200 Subject: [PATCH 09/75] feat: [DSM-142] Improve `process_batch()` phase duration coverage (#10426) Following the introduction of structured `CanisterStates` (hot vs cold canisters), almost half of the remaining DSM overhead is not covered by `mr_process_batch_phase_duration_seconds`, so it can only be bundled under "other" (the difference to `mr_process_batch_duration_seconds`). Explicitly track `read_registry()` duration (including blockmaker metrics updates, likely insignificant); and schedule/queues garbage collection plus (heavy) `canister_state_bytes` computation. --- rs/messaging/src/message_routing.rs | 54 +++++++++++++++-------------- rs/messaging/src/state_machine.rs | 35 +++++++------------ 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/rs/messaging/src/message_routing.rs b/rs/messaging/src/message_routing.rs index e737a85ee358..dbb6a0a84a6b 100644 --- a/rs/messaging/src/message_routing.rs +++ b/rs/messaging/src/message_routing.rs @@ -52,7 +52,8 @@ use ic_utils_thread::JoinOnDrop; #[cfg(test)] use mockall::automock; use prometheus::{ - Gauge, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, + Gauge, Histogram, HistogramTimer, HistogramVec, IntCounter, IntCounterVec, IntGauge, + IntGaugeVec, }; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::convert::{AsRef, TryFrom}; @@ -85,6 +86,8 @@ const STATUS_QUEUE_FULL: &str = "queue_full"; const STATUS_SUCCESS: &str = "success"; const PHASE_LOAD_STATE: &str = "load_state"; +const PHASE_READ_REGISTRY: &str = "read_registry"; +const PHASE_GARBAGE_COLLECT: &str = "garbage_collect"; const PHASE_COMMIT: &str = "commit"; /// Label for message kind: "request" or "response". @@ -299,24 +302,24 @@ pub(crate) struct MessageRoutingMetrics { /// Most recently seen certified height, per remote subnet pub(crate) remote_certified_heights: IntGaugeVec, /// Batch processing phase durations, by phase. - pub(crate) process_batch_phase_duration: HistogramVec, + process_batch_phase_duration: HistogramVec, /// Number of timed out messages. - pub(crate) timed_out_messages_total: IntCounterVec, + timed_out_messages_total: IntCounterVec, /// Number of timed out callbacks. pub(crate) timed_out_callbacks_total: IntCounter, /// Number of shed best-effort messages. - pub(crate) shed_messages_total: IntCounterVec, + shed_messages_total: IntCounterVec, /// Byte size of shed best-effort messages. - pub(crate) shed_message_bytes_total: IntCounterVec, + shed_message_bytes_total: IntCounterVec, /// Height at which the subnet last split (if during the lifetime of this /// replica process; otherwise zero). - pub(crate) subnet_split_height: IntGaugeVec, + subnet_split_height: IntGaugeVec, /// Number of blocks proposed. - pub(crate) blocks_proposed_total: IntCounter, + blocks_proposed_total: IntCounter, /// Number of blocks not proposed. - pub(crate) blocks_not_proposed_total: IntCounter, + blocks_not_proposed_total: IntCounter, /// Number of blocks not proposed by blockmaker ID. - pub(crate) blocks_not_proposed_by_blockmaker_total: IntCounterVec, + blocks_not_proposed_by_blockmaker_total: IntCounterVec, subnet_info: IntGaugeVec, subnet_size: IntGauge, @@ -526,6 +529,12 @@ impl MessageRoutingMetrics { batch_time ); } + + pub fn start_phase_timer(&self, phase: &str) -> HistogramTimer { + self.process_batch_phase_duration + .with_label_values(&[phase]) + .start_timer() + } } impl DroppedMessageMetrics for MessageRoutingMetrics { @@ -699,15 +708,6 @@ impl BatchProcessorImpl { } } - /// Adds an observation to the `METRIC_PROCESS_BATCH_PHASE_DURATION` - /// histogram for the given phase. - fn observe_phase_duration(&self, phase: &str, since: &Instant) { - self.metrics - .process_batch_phase_duration - .with_label_values(&[phase]) - .observe(since.elapsed().as_secs_f64()); - } - /// Reads registry contents required by `BatchProcessorImpl::process_batch()`. // /// # Warning @@ -1321,7 +1321,7 @@ impl BatchProcessor for BatchProcessorImpl BatchProcessor for BatchProcessorImpl BatchProcessor for BatchProcessorImpl BatchProcessor for BatchProcessorImpl BatchProcessor for BatchProcessorImpl ReplicatedState { - let since = Instant::now(); + let time_out_messages_timer = self.metrics.start_phase_timer(PHASE_TIME_OUT_MESSAGES); if batch.time > state.metadata.batch_time { state.metadata.batch_time = batch.time; @@ -190,10 +180,10 @@ impl StateMachine for StateMachineImpl { let balance_before_time_out = state.balance_with_messages(); state.time_out_messages(&self.metrics); - self.observe_phase_duration(PHASE_TIME_OUT_MESSAGES, &since); + time_out_messages_timer.observe_duration(); // Time out expired callbacks. - let since = Instant::now(); + let time_out_callbacks_timer = self.metrics.start_phase_timer(PHASE_TIME_OUT_CALLBACKS); let (timed_out_callbacks, errors) = state.time_out_callbacks(); self.metrics .timed_out_callbacks_total @@ -210,11 +200,10 @@ impl StateMachine for StateMachineImpl { } #[cfg(debug_assertions)] state.assert_balance_with_messages(balance_before_time_out); - - self.observe_phase_duration(PHASE_TIME_OUT_CALLBACKS, &since); + time_out_callbacks_timer.observe_duration(); // Preprocess messages and add messages to the induction pool through the Demux. - let since = Instant::now(); + let induction_timer = self.metrics.start_phase_timer(PHASE_INDUCTION); let current_round = ExecutionRound::from(batch.batch_number.get()); let mut state_with_messages = self.demux @@ -232,7 +221,7 @@ impl StateMachine for StateMachineImpl { .consensus_queue .append(&mut consensus_responses); - self.observe_phase_duration(PHASE_INDUCTION, &since); + induction_timer.observe_duration(); let execution_round_type = if requires_full_state_hash { ExecutionRoundType::CheckpointRound @@ -241,7 +230,7 @@ impl StateMachine for StateMachineImpl { }; // Process messages from the induction pool through the Scheduler. - let since = Instant::now(); + let execution_timer = self.metrics.start_phase_timer(PHASE_EXECUTION); let round_summary = batch.batch_summary.map(|b| ExecutionRoundSummary { next_checkpoint_round: ExecutionRound::from(b.next_checkpoint_height.get()), current_interval_length: ExecutionRound::from(b.current_interval_length.get()), @@ -263,25 +252,25 @@ impl StateMachine for StateMachineImpl { batch.batch_number ) } - self.observe_phase_duration(PHASE_EXECUTION, &since); + execution_timer.observe_duration(); // Postprocess the state: route messages into streams. - let since = Instant::now(); + let message_routing_timer = self.metrics.start_phase_timer(PHASE_MESSAGE_ROUTING); #[cfg(debug_assertions)] let balance_before_routing = state_after_execution.balance_with_messages(); let mut state_after_stream_builder = self.stream_builder.build_streams(state_after_execution); - self.observe_phase_duration(PHASE_MESSAGE_ROUTING, &since); + message_routing_timer.observe_duration(); // Shed enough messages to stay below the best-effort message memory limit. - let since = Instant::now(); + let shed_messages_timer = self.metrics.start_phase_timer(PHASE_SHED_MESSAGES); state_after_stream_builder.enforce_best_effort_message_limit( self.best_effort_message_memory_capacity, &self.metrics, ); #[cfg(debug_assertions)] state_after_stream_builder.assert_balance_with_messages(balance_before_routing); - self.observe_phase_duration(PHASE_SHED_MESSAGES, &since); + shed_messages_timer.observe_duration(); state_after_stream_builder } From 9a3b10595e794bff5ec7dee08b6c0faa0b9c26f9 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:38:40 +0200 Subject: [PATCH 10/75] chore: migration canister rejects canister migration from/to cloud engine (#10427) This PR extends the migration canister to reject canister migration requests in which the migrated or replaced canister is on a cloud engine. Currently, such a canister migration is rejected because xnet calls to a cloud engine are not allowed. This PR makes the requirement explicit so that canister migration from/to a cloud engine keeps being rejected once xnet calls to a cloud engine are allowed. --- rs/migration_canister/migration_canister.did | 1 + .../src/external_interfaces/registry.rs | 65 +++++++- rs/migration_canister/src/lib.rs | 4 + rs/migration_canister/src/validation.rs | 53 +++++-- rs/migration_canister/tests/tests.rs | 150 +++++++++++++++++- 5 files changed, 250 insertions(+), 23 deletions(-) diff --git a/rs/migration_canister/migration_canister.did b/rs/migration_canister/migration_canister.did index 882990b7633e..ada245a58595 100644 --- a/rs/migration_canister/migration_canister.did +++ b/rs/migration_canister/migration_canister.did @@ -23,6 +23,7 @@ type ValidationError = variant { MigrationsDisabled : reserved; MigratedCanisterNotReady : reserved; MigratedCanisterInsufficientCycles : reserved; + CloudEngineSubnet : record { subnet : principal }; SameSubnet : reserved; ReplacedCanisterNotStopped : reserved; }; diff --git a/rs/migration_canister/src/external_interfaces/registry.rs b/rs/migration_canister/src/external_interfaces/registry.rs index 32e0fc228b07..3a2d8ceb6745 100644 --- a/rs/migration_canister/src/external_interfaces/registry.rs +++ b/rs/migration_canister/src/external_interfaces/registry.rs @@ -8,6 +8,18 @@ use crate::{ValidationError, processing::ProcessingResult}; const REGISTRY_CANISTER_ID: &str = "rwlgt-iiaaa-aaaaa-aaaaa-cai"; +#[derive(Clone, Debug, CandidType, Deserialize, PartialEq)] +pub enum SubnetType { + #[serde(rename = "application")] + Application, + #[serde(rename = "system")] + System, + #[serde(rename = "verified_application")] + VerifiedApplication, + #[serde(rename = "cloud_engine")] + CloudEngine, +} + #[derive(Clone, Debug, CandidType, Deserialize)] struct GetSubnetForCanisterArgs { principal: Option, @@ -45,19 +57,60 @@ pub async fn get_subnet_for_canister(canister_id: Principal) -> Result Ok(subnet_id), }, - Ok(Err(e)) => { + Ok(Err(_)) => Err(ValidationError::CanisterNotFound { + canister: canister_id, + }), + Err(e) => { let msg = format!( - "Call `GetSubnetForCanisterResponse` for {} failed: {}", + "Decoding `get_subnet_for_canister` for {} failed: {:?}", canister_id, e ); println!("{}", msg); Err(ValidationError::CallFailed { reason: msg }) } + }, + } +} + +// ========================================================================= // +// `get_subnet` + +#[derive(Clone, Debug, CandidType, Deserialize)] +struct GetSubnetArgs { + subnet_id: Option, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +struct SubnetRecord { + subnet_type: SubnetType, +} + +pub async fn get_subnet(subnet_id: Principal) -> Result { + let args = GetSubnetArgs { + subnet_id: Some(subnet_id), + }; + + match Call::bounded_wait( + Principal::from_text(REGISTRY_CANISTER_ID).unwrap(), + "get_subnet", + ) + .with_arg(args) + .await + { + Err(e) => { + let msg = format!("Call `get_subnet` for {} failed: {:?}", subnet_id, e); + println!("{}", msg); + Err(ValidationError::CallFailed { reason: msg }) + } + Ok(response) => match response.candid::>() { + Ok(Ok(SubnetRecord { subnet_type })) => Ok(subnet_type), + Ok(Err(e)) => { + let msg = format!("Call `get_subnet` for {} failed: {}", subnet_id, e); + println!("{}", msg); + Err(ValidationError::CallFailed { reason: msg }) + } Err(e) => { - let msg = format!( - "Decoding `get_subnet_for_canister` for {} failed: {:?}", - canister_id, e - ); + let msg = format!("Decoding `get_subnet` for {} failed: {:?}", subnet_id, e); println!("{}", msg); Err(ValidationError::CallFailed { reason: msg }) } diff --git a/rs/migration_canister/src/lib.rs b/rs/migration_canister/src/lib.rs index dca1a85e3347..59eb6505a562 100644 --- a/rs/migration_canister/src/lib.rs +++ b/rs/migration_canister/src/lib.rs @@ -73,6 +73,10 @@ pub enum ValidationError { ReplacedCanisterNotStopped(Reserved), ReplacedCanisterHasSnapshots(Reserved), MigratedCanisterInsufficientCycles(Reserved), + #[strum(to_string = "ValidationError::CloudEngineSubnet {{ subnet: {subnet} }}")] + CloudEngineSubnet { + subnet: Principal, + }, #[strum(to_string = "ValidationError::CallFailed {{ reason: {reason} }}")] CallFailed { reason: String, diff --git a/rs/migration_canister/src/validation.rs b/rs/migration_canister/src/validation.rs index 2014f7218ab7..5da4479f451d 100644 --- a/rs/migration_canister/src/validation.rs +++ b/rs/migration_canister/src/validation.rs @@ -20,7 +20,7 @@ use crate::{ CanisterStatusResponse, CanisterStatusType, assert_no_snapshots, canister_status, get_canister_info, }, - registry::get_subnet_for_canister, + registry::{SubnetType, get_subnet, get_subnet_for_canister}, }, processing::ProcessingResult, }; @@ -72,21 +72,42 @@ pub async fn validate_request( replaced_canister: Principal, caller: Principal, ) -> Result<(Request, Vec), ValidationError> { - // We first check if the caller is authorized (i.e., - // if the caller is a controller of both the migrated and replaced canisters) - // before acquiring locks for the migrated and replaced canisters - // to prevent unauthorized callers from acquiring the lock - // and blocking authorized callers from performing canister migration. - // 1. The migrated canister must not be equal to the replaced canister. if migrated_canister == replaced_canister { return Err(ValidationError::SameSubnet(Reserved)); } - // 2. Is the caller controller of the migrated canister? + // The scope ensures that `migrated_canister_subnet` and `replaced_canister_subnet` + // cannot be used below (step 6 re-fetches the subnets after acquiring the locks). + { + // 2. Are the migrated and replaced canisters on cloud engine subnets? + // It is safe to perform this check before acquiring locks because the fact that + // neither the migrated nor the replaced canister is on a cloud engine subnet + // cannot change later. + let migrated_canister_subnet = get_subnet_for_canister(migrated_canister).await?; + let replaced_canister_subnet = get_subnet_for_canister(replaced_canister).await?; + if get_subnet(migrated_canister_subnet).await? == SubnetType::CloudEngine { + return Err(ValidationError::CloudEngineSubnet { + subnet: migrated_canister_subnet, + }); + } + if get_subnet(replaced_canister_subnet).await? == SubnetType::CloudEngine { + return Err(ValidationError::CloudEngineSubnet { + subnet: replaced_canister_subnet, + }); + } + } + + // We check if the caller is authorized (i.e., + // if the caller is a controller of both the migrated and replaced canisters) + // before acquiring locks for the migrated and replaced canisters + // to prevent unauthorized callers from acquiring the lock + // and blocking authorized callers from performing canister migration. + + // 3. Is the caller controller of the migrated canister? let migrated_canister_status = check_controllers_and_get_status(migrated_canister, caller).await?; - // 3. Is the caller controller of the replaced canister? + // 4. Is the caller controller of the replaced canister? let replaced_canister_status = check_controllers_and_get_status(replaced_canister, caller).await?; @@ -104,7 +125,7 @@ pub async fn validate_request( }); }; - // 4. Is any of these canisters already in a migration process? + // 5. Is any of these canisters already in a migration process? for request in list_by(|_| true) { if let Some(id) = request .request() @@ -113,30 +134,30 @@ pub async fn validate_request( return Err(ValidationError::MigrationInProgress { canister: id }); } } - // 5. Are the migrated and replaced canisters on the same subnet? + // 6. Are the migrated and replaced canisters on the same subnet? let migrated_canister_subnet = get_subnet_for_canister(migrated_canister).await?; let replaced_canister_subnet = get_subnet_for_canister(replaced_canister).await?; if migrated_canister_subnet == replaced_canister_subnet { return Err(ValidationError::SameSubnet(Reserved)); } - // 6. Is the migrated canister stopped? + // 7. Is the migrated canister stopped? if migrated_canister_status.status != CanisterStatusType::Stopped { return Err(ValidationError::MigratedCanisterNotStopped(Reserved)); } - // 7. Is the migrated canister ready for migration? + // 8. Is the migrated canister ready for migration? if !migrated_canister_status.ready_for_migration { return Err(ValidationError::MigratedCanisterNotReady(Reserved)); } - // 8. Is the replaced canister stopped? + // 9. Is the replaced canister stopped? if replaced_canister_status.status != CanisterStatusType::Stopped { return Err(ValidationError::ReplacedCanisterNotStopped(Reserved)); } - // 9. Does the replaced canister have snapshots? + // 10. Does the replaced canister have snapshots? assert_no_snapshots(replaced_canister).await.into_result( "Call to management canister `list_canister_snapshots` failed. Try again later.", )?; - // 10. Does the migrated canister have sufficient cycles for the migration? + // 11. Does the migrated canister have sufficient cycles for the migration? if migrated_canister_status.cycles < CYCLES_COST_PER_MIGRATION { return Err(ValidationError::MigratedCanisterInsufficientCycles( Reserved, diff --git a/rs/migration_canister/tests/tests.rs b/rs/migration_canister/tests/tests.rs index e1a0225100e1..822a3d5caaa0 100644 --- a/rs/migration_canister/tests/tests.rs +++ b/rs/migration_canister/tests/tests.rs @@ -13,7 +13,10 @@ use ic_universal_canister::{CallArgs, UNIVERSAL_CANISTER_WASM, wasm}; use itertools::Itertools; use pocket_ic::{ CreateCanisterParams, CreateCanisterPlacement, PocketIcBuilder, - common::rest::{IcpFeatures, IcpFeaturesConfig}, + common::rest::{ + CanisterCyclesCostSchedule, ExtendedSubnetConfigSet, IcpFeatures, IcpFeaturesConfig, + SubnetSpec, + }, nonblocking::PocketIc, }; use prometheus_parse::Scrape; @@ -53,6 +56,7 @@ pub enum ValidationError { ReplacedCanisterNotStopped(Reserved), ReplacedCanisterHasSnapshots(Reserved), MigratedCanisterInsufficientCycles(Reserved), + CloudEngineSubnet { subnet: Principal }, CallFailed { reason: String }, } @@ -1784,3 +1788,147 @@ async fn parallel_validations() { assert_eq!(not_controller_counter, 200); assert_eq!(rate_limited_counter, 60); } + +async fn migrate_with_cloud_engine_subnet( + migrated_on_cloud_engine: bool, + replaced_on_cloud_engine: bool, +) -> Result<(), ValidationError> { + let pic = PocketIcBuilder::new_with_config(ExtendedSubnetConfigSet { + application: vec![SubnetSpec::default(), SubnetSpec::default()], + cloud_engine: vec![ + SubnetSpec::default().with_cost_schedule(CanisterCyclesCostSchedule::Free), + SubnetSpec::default().with_cost_schedule(CanisterCyclesCostSchedule::Free), + ], + ..Default::default() + }) + .with_icp_features(IcpFeatures { + registry: Some(IcpFeaturesConfig::DefaultConfig), + ..Default::default() + }) + .build_async() + .await; + + let system_controller = Principal::anonymous(); + let c1 = Principal::self_authenticating(vec![1]); + + let registry_wasm = Project::cargo_bin_maybe_from_env("registry-canister", &[]); + pic.upgrade_canister( + REGISTRY_CANISTER_ID.into(), + registry_wasm.bytes(), + vec![], + Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()), + ) + .await + .unwrap(); + + let migration_canister_wasm = Project::cargo_bin_maybe_from_env("migration-canister", &[]); + pic.create_canister_with_id( + Some(system_controller), + Some(CanisterSettings { + controllers: Some(vec![system_controller]), + ..Default::default() + }), + MIGRATION_CANISTER_ID.into(), + ) + .await + .unwrap(); + pic.install_canister( + MIGRATION_CANISTER_ID.into(), + migration_canister_wasm.bytes(), + Encode!(&MigrationCanisterInitArgs { allowlist: None }).unwrap(), + Some(system_controller), + ) + .await; + + let topology = pic.topology().await; + let app_subnets = topology.get_app_subnets(); + let cloud_engine_subnets = topology.get_cloud_engines(); + + let controllers = vec![c1, MIGRATION_CANISTER_ID.into()]; + + let migrated_subnet = if migrated_on_cloud_engine { + cloud_engine_subnets[0] + } else { + app_subnets[0] + }; + let migrated_canister = pic + .create_canister_with_params( + Some(c1), + CreateCanisterParams { + cycles: None, + settings: Some(CanisterSettings { + controllers: Some(controllers.clone()), + ..Default::default() + }), + placement: Some(CreateCanisterPlacement::SubnetId(migrated_subnet)), + }, + ) + .await + .unwrap(); + pic.add_cycles(migrated_canister, u128::MAX / 2).await; + pic.stop_canister(migrated_canister, Some(c1)) + .await + .unwrap(); + + let replaced_subnet = if replaced_on_cloud_engine { + cloud_engine_subnets[1] + } else { + app_subnets[1] + }; + let replaced_canister = pic + .create_canister_with_params( + Some(c1), + CreateCanisterParams { + cycles: None, + settings: Some(CanisterSettings { + controllers: Some(controllers), + ..Default::default() + }), + placement: Some(CreateCanisterPlacement::SubnetId(replaced_subnet)), + }, + ) + .await + .unwrap(); + pic.add_cycles(replaced_canister, u128::MAX / 2).await; + pic.stop_canister(replaced_canister, Some(c1)) + .await + .unwrap(); + + migrate_canister( + &pic, + c1, + &MigrateCanisterArgs { + migrated_canister_id: migrated_canister, + replaced_canister_id: replaced_canister, + }, + ) + .await +} + +#[tokio::test] +async fn validation_fails_cloud_engine_subnet() { + let result = migrate_with_cloud_engine_subnet(false, true).await; + let Err(ValidationError::CloudEngineSubnet { .. }) = result else { + panic!("unexpected result: {:?}", result) + }; + + let result = migrate_with_cloud_engine_subnet(true, false).await; + let Err(ValidationError::CloudEngineSubnet { .. }) = result else { + panic!("unexpected result: {:?}", result) + }; +} + +#[tokio::test] +async fn validation_succeeds_both_app_subnets() { + migrate_with_cloud_engine_subnet(false, false) + .await + .unwrap(); +} + +#[tokio::test] +async fn validation_fails_both_cloud_engine_subnets() { + let result = migrate_with_cloud_engine_subnet(true, true).await; + let Err(ValidationError::CloudEngineSubnet { .. }) = result else { + panic!("unexpected result: {:?}", result) + }; +} From 5fe61d74bba6d0b4290ace88577e77d7dd01f8ad Mon Sep 17 00:00:00 2001 From: Leo Eichhorn <99166915+eichhorl@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:18:15 +0200 Subject: [PATCH 11/75] feat: IC-1943 Include refund shares in HTTP outcalls payload (#10310) ## Background Currently, the price for an HTTP outcall is calculated and charged upfront. In the future, we want to implement a "pay-as-you-go" pricing model. This model assigns a fraction (budget) of the total attached cycles to each participating replica (per-replica allowance). As the request passes through the target server and the transform function, the HTTP adapter of each replica subtracts cycles from its own budget, based on the amount of resources it consumed (download time, downloaded bytes, transform instructions). In the end, the amount of remaining cycles (refund) is passed back to the replica together with the HTTP response. ## Proposed Changes With this PR, we let the replica include (and sign) this refund as part of the gossiped response share. Before, each replica only signed the response metadata which should be equal across all responses in order to reach consensus. Now, the signed messages may be different even across agreeing replicas, as each of them could sign a different refund share. Therefore, we change the structure of the aggregated HTTP response proof. Previously, this was just the metadata of the response and a collection of individual signatures over it. Now, it is the metadata and a collection of individual signatures and refund receipts of each replica. In order to verify the proof, the individual messages consisting of both the metadata and the refund share have to be reconstructed, such that the signature can be verified. For that reason we additionally switch signature verification of aggregated responses to use the new batch verification API for multiple messages, similar to what was done in https://github.com/dfinity/ic/pull/10345. As a side effect, this comes with some performance improvements for payload validation: ``` canister_http_payload_verification/mixed_subnet34 time: [146.72 ms 148.46 ms 150.26 ms] change: [-14.887% -13.555% -12.243%] (p = 0.00 < 0.05) Performance has improved. canister_http_payload_verification/many_replicated_responses_subnet34 time: [225.27 ms 227.44 ms 229.63 ms] change: [-17.525% -16.435% -15.417%] (p = 0.00 < 0.05) Performance has improved. canister_http_payload_verification/many_non_replicated_responses_subnet34 time: [11.124 ms 11.263 ms 11.406 ms] change: [-61.100% -60.382% -59.642%] (p = 0.00 < 0.05) Performance has improved. canister_http_payload_verification/many_divergence_responses_subnet34 time: [116.12 ms 117.52 ms 118.94 ms] change: [-4.0609% -2.4873% -0.9067%] (p = 0.00 < 0.05) Change within noise threshold. canister_http_payload_verification/many_flexible_responses_subnet34 time: [253.98 ms 256.09 ms 258.22 ms] change: [-3.1565% -2.0740% -0.8815%] (p = 0.00 < 0.05) Change within noise threshold. ``` Additionally, during payload validation, we verify that none of the refund shares exceed the per-replica allowance. Note that in the current (legacy) pricing model, nothing is refunded yet, so the allowance, and the returned refund is always 0. --------- Co-authored-by: IDX GitHub Automation --- Cargo.lock | 2 + rs/artifact_pool/src/canister_http_pool.rs | 22 +- rs/consensus/utils/src/crypto.rs | 15 +- rs/https_outcalls/consensus/BUILD.bazel | 3 + rs/https_outcalls/consensus/Cargo.toml | 3 + .../consensus/benches/payload_validation.rs | 110 ++-- rs/https_outcalls/consensus/src/gossip.rs | 14 +- .../consensus/src/payload_builder.rs | 175 +++---- .../consensus/src/payload_builder/tests.rs | 363 ++++++++++--- .../consensus/src/payload_builder/utils.rs | 170 +++--- .../consensus/src/pool_manager.rs | 489 ++++++++++++------ rs/interfaces/mocks/src/crypto.rs | 28 +- rs/interfaces/src/canister_http.rs | 7 + rs/interfaces/src/crypto.rs | 10 +- rs/protobuf/def/types/v1/canister_http.proto | 1 + rs/protobuf/src/gen/types/types.v1.rs | 2 + rs/state_machine_tests/src/lib.rs | 30 +- rs/types/types/src/batch/canister_http.rs | 133 +++-- rs/types/types/src/canister_http.rs | 118 ++++- rs/types/types/src/crypto/hash/tests.rs | 15 +- rs/types/types/src/crypto/sign.rs | 6 +- rs/types/types/src/exhaustive.rs | 4 +- 22 files changed, 1159 insertions(+), 561 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd45607a8be1..4b85fc3a9c9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10280,12 +10280,14 @@ dependencies = [ "ic-registry-subnet-type", "ic-replicated-state", "ic-test-utilities", + "ic-test-utilities-consensus", "ic-test-utilities-logger", "ic-test-utilities-registry", "ic-test-utilities-state", "ic-test-utilities-time", "ic-test-utilities-types", "ic-types", + "ic-types-cycles", "ic-utils 0.9.0", "mockall", "prometheus", diff --git a/rs/artifact_pool/src/canister_http_pool.rs b/rs/artifact_pool/src/canister_http_pool.rs index 1cb9f1c2b755..f3cdcd7e86b1 100644 --- a/rs/artifact_pool/src/canister_http_pool.rs +++ b/rs/artifact_pool/src/canister_http_pool.rs @@ -234,7 +234,10 @@ mod tests { use ic_types::{ CanisterId, RegistryVersion, ReplicaVersion, artifact::IdentifiableArtifact, - canister_http::{CanisterHttpResponseContent, CanisterHttpResponseMetadata}, + canister_http::{ + CanisterHttpPaymentReceipt, CanisterHttpResponseContent, CanisterHttpResponseMetadata, + CanisterHttpResponseReceipt, + }, crypto::{CryptoHash, Signed}, messages::CallbackId, signature::BasicSignature, @@ -259,13 +262,16 @@ mod tests { fn fake_share(id: u64) -> CanisterHttpResponseShare { Signed { - content: CanisterHttpResponseMetadata { - id: CallbackId::from(id), - content_hash: CryptoHashOf::from(CryptoHash(vec![1, 2, 3])), - content_size: 42, - is_reject: false, - registry_version: RegistryVersion::from(id), - replica_version: ReplicaVersion::default(), + content: CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(id), + content_hash: CryptoHashOf::from(CryptoHash(vec![1, 2, 3])), + content_size: 42, + is_reject: false, + registry_version: RegistryVersion::from(id), + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }, signature: BasicSignature::fake(node_test_id(id)), } diff --git a/rs/consensus/utils/src/crypto.rs b/rs/consensus/utils/src/crypto.rs index d2c6f38fa17e..39f919823c4f 100644 --- a/rs/consensus/utils/src/crypto.rs +++ b/rs/consensus/utils/src/crypto.rs @@ -1,7 +1,7 @@ use ic_interfaces::{crypto::*, validation::ValidationResult}; use ic_types::{ NodeId, RegistryVersion, - canister_http::CanisterHttpResponseMetadata, + canister_http::CanisterHttpResponseReceipt, consensus::{ BlockMetadata, CatchUpContent, FinalizationContent, NotarizationContent, RandomBeaconContent, RandomTapeContent, dkg, @@ -430,12 +430,8 @@ pub trait ConsensusCrypto: + SignVerify, NiDkgId> + SignVerify, RegistryVersion> + SignVerify< - CanisterHttpResponseMetadata, - BasicSignature, - RegistryVersion, - > + SignVerify< - CanisterHttpResponseMetadata, - BasicSignature, + CanisterHttpResponseReceipt, + BasicSignature, RegistryVersion, > + Aggregate< NotarizationContent, @@ -467,11 +463,6 @@ pub trait ConsensusCrypto: ThresholdSignatureShare, NiDkgId, ThresholdSignature, - > + Aggregate< - CanisterHttpResponseMetadata, - BasicSignature, - RegistryVersion, - BasicSignatureBatch, > + Crypto + Send + Sync diff --git a/rs/https_outcalls/consensus/BUILD.bazel b/rs/https_outcalls/consensus/BUILD.bazel index a3fbf462f3cf..02fa215eb284 100644 --- a/rs/https_outcalls/consensus/BUILD.bazel +++ b/rs/https_outcalls/consensus/BUILD.bazel @@ -29,6 +29,7 @@ rust_library( "//rs/registry/helpers", "//rs/registry/subnet_type", "//rs/replicated_state", + "//rs/types/cycles", "//rs/types/management_canister_types", "//rs/types/types", "//rs/utils", @@ -69,11 +70,13 @@ rust_test( "//rs/registry/subnet_type", "//rs/replicated_state", "//rs/test_utilities", + "//rs/test_utilities/consensus", "//rs/test_utilities/logger", "//rs/test_utilities/registry", "//rs/test_utilities/state", "//rs/test_utilities/time", "//rs/test_utilities/types", + "//rs/types/cycles", "//rs/types/management_canister_types", "//rs/types/types", "//rs/utils", diff --git a/rs/https_outcalls/consensus/Cargo.toml b/rs/https_outcalls/consensus/Cargo.toml index 570e8d3907dc..c1fc680377ba 100644 --- a/rs/https_outcalls/consensus/Cargo.toml +++ b/rs/https_outcalls/consensus/Cargo.toml @@ -23,6 +23,7 @@ ic-registry-client-helpers = { path = "../../registry/helpers" } ic-registry-subnet-type = { path = "../../registry/subnet_type" } ic-replicated-state = { path = "../../replicated_state" } ic-types = { path = "../../types/types" } +ic-types-cycles = { path = "../../types/cycles" } ic-utils = { path = "../../utils" } prometheus = { workspace = true } rand = { workspace = true } @@ -37,11 +38,13 @@ ic-error-types = { path = "../../../packages/ic-error-types" } ic-interfaces-mocks = { path = "../../interfaces/mocks" } ic-registry-subnet-features = { path = "../../registry/subnet_features" } ic-test-utilities = { path = "../../test_utilities" } +ic-test-utilities-consensus = { path = "../../test_utilities/consensus" } ic-test-utilities-logger = { path = "../../test_utilities/logger" } ic-test-utilities-registry = { path = "../../test_utilities/registry" } ic-test-utilities-state = { path = "../../test_utilities/state" } ic-test-utilities-time = { path = "../../test_utilities/time" } ic-test-utilities-types = { path = "../../test_utilities/types" } +ic-types-cycles = { path = "../../types/cycles" } assert_matches = { workspace = true } criterion = { workspace = true } mockall = { workspace = true } diff --git a/rs/https_outcalls/consensus/benches/payload_validation.rs b/rs/https_outcalls/consensus/benches/payload_validation.rs index d3245ef5cc8c..3e45e797b1e3 100644 --- a/rs/https_outcalls/consensus/benches/payload_validation.rs +++ b/rs/https_outcalls/consensus/benches/payload_validation.rs @@ -29,15 +29,16 @@ use ic_types::{ ValidationContext, }, canister_http::{ - CanisterHttpMethod, CanisterHttpRequestContext, CanisterHttpResponse, - CanisterHttpResponseContent, CanisterHttpResponseDivergence, CanisterHttpResponseMetadata, - CanisterHttpResponseShare, CanisterHttpResponseWithConsensus, PricingVersion, RefundStatus, - Replication, + CanisterHttpMethod, CanisterHttpPaymentReceipt, CanisterHttpRequestContext, + CanisterHttpResponse, CanisterHttpResponseContent, CanisterHttpResponseDivergence, + CanisterHttpResponseMetadata, CanisterHttpResponseProof, CanisterHttpResponseReceipt, + CanisterHttpResponseShare, CanisterHttpResponseSignature, + CanisterHttpResponseWithConsensus, PricingVersion, RefundStatus, Replication, }, consensus::get_faults_tolerated, - crypto::{BasicSigOf, Signed, crypto_hash}, + crypto::{BasicSigOf, crypto_hash}, messages::CallbackId, - signature::{BasicSignature, BasicSignatureBatch}, + signature::BasicSignature, time::UNIX_EPOCH, }; @@ -241,8 +242,8 @@ fn build_target( } } -/// Helper that signs `CanisterHttpResponseMetadata` with a committee node's -/// crypto component. +/// Helper that signs `CanisterHttpResponseReceipt` with a committee +/// node's crypto component. struct Signer<'a> { crypto: &'a [TempCryptoComponent], } @@ -251,11 +252,11 @@ impl Signer<'_> { fn sign( &self, node_index: usize, - metadata: &CanisterHttpResponseMetadata, - ) -> BasicSigOf { + receipt_share: &CanisterHttpResponseReceipt, + ) -> BasicSigOf { self.crypto[node_index] - .sign_basic(metadata) - .expect("failed to sign response metadata") + .sign_basic(receipt_share) + .expect("failed to sign response receipt share") } } @@ -284,6 +285,48 @@ impl<'a> PayloadAssembler<'a> { id } + /// Builds a node's contribution to an aggregated proof: a default (zero + /// refund) payment receipt together with that node's signature over the + /// corresponding receipt share. + fn signature( + &self, + signer: &Signer, + node: usize, + metadata: &CanisterHttpResponseMetadata, + ) -> CanisterHttpResponseSignature { + let receipt_share = CanisterHttpResponseReceipt { + metadata: metadata.clone(), + payment_receipt: CanisterHttpPaymentReceipt::default(), + }; + let signature = signer.sign(node, &receipt_share); + CanisterHttpResponseSignature { + payment_receipt: receipt_share.payment_receipt, + signature, + } + } + + /// Builds a single signed [`CanisterHttpResponseShare`] (receipt share with + /// a default, zero-refund payment receipt) for the given node. + fn share( + &self, + signer: &Signer, + node: usize, + metadata: CanisterHttpResponseMetadata, + ) -> CanisterHttpResponseShare { + let receipt_share = CanisterHttpResponseReceipt { + metadata, + payment_receipt: CanisterHttpPaymentReceipt::default(), + }; + let signature = signer.sign(node, &receipt_share); + CanisterHttpResponseShare { + signature: BasicSignature { + signature, + signer: self.committee[node], + }, + content: receipt_share, + } + } + fn assemble(&mut self, signer: &Signer) -> CanisterHttpPayload { let subnet_size = self.config.subnet_size; let threshold = subnet_size - get_faults_tolerated(subnet_size); @@ -307,14 +350,19 @@ impl<'a> PayloadAssembler<'a> { for _ in 0..self.config.num_replicated { let callback_id = self.alloc_callback_id(); let (response, metadata) = response_and_metadata(callback_id, success_content()); - let signatures_map = (0..threshold) - .map(|node| (self.committee[node], signer.sign(node, &metadata))) + let signatures = (0..threshold) + .map(|node| { + ( + self.committee[node], + self.signature(signer, node, &metadata), + ) + }) .collect(); responses.push(CanisterHttpResponseWithConsensus { content: response, - proof: Signed { - content: metadata, - signature: BasicSignatureBatch { signatures_map }, + proof: CanisterHttpResponseProof { + metadata, + signatures, }, }); self.contexts.push(( @@ -328,16 +376,16 @@ impl<'a> PayloadAssembler<'a> { let callback_id = self.alloc_callback_id(); let designated = (callback_id as usize) % subnet_size; let (response, metadata) = response_and_metadata(callback_id, success_content()); - let mut signatures_map = BTreeMap::new(); - signatures_map.insert( + let mut signatures = BTreeMap::new(); + signatures.insert( self.committee[designated], - signer.sign(designated, &metadata), + self.signature(signer, designated, &metadata), ); responses.push(CanisterHttpResponseWithConsensus { content: response, - proof: Signed { - content: metadata, - signature: BasicSignatureBatch { signatures_map }, + proof: CanisterHttpResponseProof { + metadata, + signatures, }, }); self.contexts.push(( @@ -358,13 +406,7 @@ impl<'a> PayloadAssembler<'a> { format!("divergent-{callback_id}-{node}").into_bytes(), ); let (_, metadata) = response_and_metadata(callback_id, content); - Signed { - signature: BasicSignature { - signature: signer.sign(node, &metadata), - signer: self.committee[node], - }, - content: metadata, - } + self.share(signer, node, metadata) }) .collect(); divergence_responses.push(CanisterHttpResponseDivergence { shares }); @@ -385,13 +427,7 @@ impl<'a> PayloadAssembler<'a> { let entries = (0..threshold) .map(|node| FlexibleCanisterHttpResponseWithProof { response: response.clone(), - proof: Signed { - signature: BasicSignature { - signature: signer.sign(node, &metadata), - signer: self.committee[node], - }, - content: metadata.clone(), - }, + proof: self.share(signer, node, metadata.clone()), }) .collect(); flexible_responses.push(FlexibleCanisterHttpResponses { diff --git a/rs/https_outcalls/consensus/src/gossip.rs b/rs/https_outcalls/consensus/src/gossip.rs index 45deb32d11f6..818fd4b61ae3 100644 --- a/rs/https_outcalls/consensus/src/gossip.rs +++ b/rs/https_outcalls/consensus/src/gossip.rs @@ -61,12 +61,12 @@ impl BouncerFactory }; let log = self.log.clone(); Box::new(move |id: &'_ CanisterHttpResponseId| { - if id.content.registry_version != registry_version { + if id.content.registry_version() != registry_version { warn!( log, "Dropping canister http response share with callback id: {}, because registry version {} does not match expected version {}", - id.content.id, - id.content.registry_version, + id.content.id(), + id.content.registry_version(), registry_version ); return BouncerValue::Unwanted; @@ -83,12 +83,12 @@ impl BouncerFactory // not higher that `MAX_NUMBER_OF_REQUESTS_AHEAD`. // Receiving an callback Id higher is possible because the priority fn is updated periodically (every 3s) with the latest state // and can therefore store stale `known_request_ids` and stale `next_callback_id`. - if known_request_ids.contains(&id.content.id) - || (id.content.id >= next_callback_id - && id.content.id <= highest_accepted_request_id) + if known_request_ids.contains(&id.content.id()) + || (id.content.id() >= next_callback_id + && id.content.id() <= highest_accepted_request_id) { BouncerValue::Wants - } else if id.content.id > highest_accepted_request_id { + } else if id.content.id() > highest_accepted_request_id { BouncerValue::MaybeWantsLater } else { BouncerValue::Unwanted diff --git a/rs/https_outcalls/consensus/src/payload_builder.rs b/rs/https_outcalls/consensus/src/payload_builder.rs index 721ac540f24a..fe4203e45852 100644 --- a/rs/https_outcalls/consensus/src/payload_builder.rs +++ b/rs/https_outcalls/consensus/src/payload_builder.rs @@ -5,8 +5,8 @@ use crate::{ payload_builder::{ parse::bytes_to_payload, utils::{ - FlexibleFindResult, ResponseShareSigInput, estimate_response_with_consensus_size, - find_flexible_result, find_fully_replicated_response, find_non_replicated_response, + FlexibleFindResult, ResponseShareSigInput, find_flexible_result, + find_fully_replicated_response, find_non_replicated_response, group_shares_by_callback_id, grouped_shares_meet_divergence_criteria, response_share_sig_inputs, validate_flexible_response_with_proof, validate_response_share, @@ -40,7 +40,7 @@ use ic_metrics::MetricsRegistry; use ic_registry_client_helpers::subnet::SubnetRegistry; use ic_replicated_state::ReplicatedState; use ic_types::{ - CountBytes, Height, NodeId, NumBytes, RegistryVersion, SubnetId, + CountBytes, Height, NodeId, NumBytes, SubnetId, batch::{ CanisterHttpPayload, ConsensusResponse, FlexibleCanisterHttpError, FlexibleCanisterHttpResponseWithProof, FlexibleCanisterHttpResponses, @@ -48,17 +48,15 @@ use ic_types::{ }, canister_http::{ CANISTER_HTTP_MAX_RESPONSES_PER_BLOCK, CANISTER_HTTP_TIMEOUT_INTERVAL, - CanisterHttpResponse, CanisterHttpResponseContent, CanisterHttpResponseDivergence, - CanisterHttpResponseMetadata, CanisterHttpResponseWithConsensus, Replication, + CanisterHttpResponseContent, CanisterHttpResponseDivergence, CanisterHttpResponseShare, + Replication, }, consensus::Committee, - crypto::Signed, messages::{CallbackId, Payload, RejectContext}, registry::RegistryClientError, - signature::BasicSignature, }; use std::{ - collections::{BTreeMap, BTreeSet, HashSet}, + collections::{BTreeMap, HashSet}, sync::{Arc, RwLock}, }; @@ -135,35 +133,6 @@ impl CanisterHttpPayloadBuilderImpl { .map(|features| features.unwrap_or_default().http_requests) } - /// Aggregates the signature and creates the [`CanisterHttpResponseWithConsensus`] message. - fn aggregate( - &self, - registry_version: RegistryVersion, - metadata: CanisterHttpResponseMetadata, - shares: BTreeSet>, - content: CanisterHttpResponse, - ) -> Option { - match self - .crypto - .aggregate(shares.iter().collect(), registry_version) - { - Err(err) => { - warn!( - self.log, - "Failed to aggregate signature for CanisterHttpResponse: {:?}", err - ); - None - } - Ok(signature) => Some(CanisterHttpResponseWithConsensus { - content, - proof: Signed { - content: metadata, - signature, - }, - }), - } - } - fn get_canister_http_payload_impl( &self, height: Height, @@ -220,7 +189,7 @@ impl CanisterHttpPayloadBuilderImpl { let mut accumulated_size = 0; let mut responses_included = 0; - let mut candidates = vec![]; + let mut responses = vec![]; let mut timeouts = vec![]; let mut divergence_responses = vec![]; let mut flexible_responses = vec![]; @@ -230,9 +199,7 @@ impl CanisterHttpPayloadBuilderImpl { let mut total_share_count = 0; let mut active_shares = 0; - // Since aggregating signatures is potentially expensive (currently for - // BasicSignatures it is not expensive), we pick the candidates first - // (under the pool lock), then aggregate in a separate step. + // Pick the candidates under the pool lock. { let pool_access = self.pool.read().unwrap(); @@ -243,14 +210,14 @@ impl CanisterHttpPayloadBuilderImpl { total_share_count += 1; }) // Filter out shares with the wrong registry version - .filter(|&share| share.content.registry_version == consensus_registry_version) + .filter(|&share| share.content.registry_version() == consensus_registry_version) .inspect(|_| { active_shares += 1; }) // Filter out shares for responses to requests that already have // responses in the block chain up to the point we are creating a // new payload. - .filter(|&response| !delivered_ids.contains(&response.content.id)); + .filter(|&share| !delivered_ids.contains(&share.content.id())); // Group the shares by their metadata let shares_by_callback_id = group_shares_by_callback_id(share_candidates); @@ -298,14 +265,13 @@ impl CanisterHttpPayloadBuilderImpl { }; match &request.replication { Replication::FullyReplicated => { - if let Some((metadata, shares, content)) = + if let Some(response) = find_fully_replicated_response(grouped_shares, threshold, &*pool_access) { - let candidate_size = - estimate_response_with_consensus_size(&metadata, &shares, &content); + let candidate_size = response.count_bytes(); let size = NumBytes::new((accumulated_size + candidate_size) as u64); if size < max_payload_size { - candidates.push((metadata, shares, content)); + responses.push(response); responses_included += 1; accumulated_size += candidate_size; } @@ -330,16 +296,15 @@ impl CanisterHttpPayloadBuilderImpl { } } Replication::NonReplicated(designated_node_id) => { - if let Some((metadata, shares, content)) = find_non_replicated_response( + if let Some(response) = find_non_replicated_response( grouped_shares, designated_node_id, &*pool_access, ) { - let candidate_size = - estimate_response_with_consensus_size(&metadata, &shares, &content); + let candidate_size = response.count_bytes(); let size = NumBytes::new((accumulated_size + candidate_size) as u64); if size < max_payload_size { - candidates.push((metadata, shares, content)); + responses.push(response); responses_included += 1; accumulated_size += candidate_size; } @@ -381,12 +346,7 @@ impl CanisterHttpPayloadBuilderImpl { } CanisterHttpPayload { - responses: candidates - .into_iter() - .filter_map(|(metadata, shares, content)| { - self.aggregate(consensus_registry_version, metadata, shares, content) - }) - .collect(), + responses, timeouts, divergence_responses, flexible_responses, @@ -467,6 +427,21 @@ impl CanisterHttpPayloadBuilderImpl { CanisterHttpPayloadValidationFailure::ConsensusRegistryVersionUnavailable, ))?; + let committee = self + .membership + .get_canister_http_committee(height) + .map_err(|_| { + CanisterHttpPayloadValidationError::ValidationFailed( + CanisterHttpPayloadValidationFailure::Membership, + ) + })?; + + // Shares reconstructed from aggregated response proofs. + let mut reconstructed_shares: Vec = Vec::new(); + // Accumulates all signatures in the payload, so that they can be checked + // in a single batched multi-message verification call at the very end. + let mut sig_inputs: Vec = Vec::new(); + // Check conditions on individual responses for response in &payload.responses { // Check that response is consistent @@ -474,11 +449,11 @@ impl CanisterHttpPayloadBuilderImpl { .map_err(CanisterHttpPayloadValidationError::InvalidArtifact)?; // Validate response against consensus registry version - if response.proof.content.registry_version != consensus_registry_version { + if response.proof.registry_version() != consensus_registry_version { return invalid_artifact( InvalidCanisterHttpPayloadReason::RegistryVersionMismatch { expected: consensus_registry_version, - received: response.proof.content.registry_version, + received: response.proof.registry_version(), }, ); } @@ -489,21 +464,7 @@ impl CanisterHttpPayloadBuilderImpl { response.content.id, )); } - } - let committee = self - .membership - .get_canister_http_committee(height) - .map_err(|_| { - CanisterHttpPayloadValidationError::ValidationFailed( - CanisterHttpPayloadValidationFailure::Membership, - ) - })?; - - // Verify the signatures - // NOTE: We do this in a separate loop because this check is expensive and we want to - // do all the cheap checks first - for response in &payload.responses { let callback_id = response.content.id; let request_context = http_contexts.get(&callback_id).ok_or( CanisterHttpPayloadValidationError::InvalidArtifact( @@ -537,8 +498,7 @@ impl CanisterHttpPayloadBuilderImpl { let (valid_signers, invalid_signers): (Vec, Vec) = response .proof - .signature - .signatures_map + .signatures .keys() .cloned() .partition(|signer| effective_committee.iter().any(|id| id == signer)); @@ -558,28 +518,22 @@ impl CanisterHttpPayloadBuilderImpl { }); } - self.crypto - .verify_aggregate(&response.proof, consensus_registry_version) - .map_err(|err| { - CanisterHttpPayloadValidationError::InvalidArtifact( - InvalidCanisterHttpPayloadReason::SignatureError(Box::new(err)), - ) - })?; + // Enforce the per-replica refund allowance on every receipt in the proof. + for sig in response.proof.signatures.values() { + utils::check_refund_allowance( + &sig.payment_receipt, + request_context.refund_status.per_replica_allowance, + ) + .map_err(CanisterHttpPayloadValidationError::InvalidArtifact)?; + } + // Reconstruct the per-signer shares from the response proof. + reconstructed_shares.extend(utils::reconstruct_individual_shares(&response.proof)); } - let faults_tolerated = match self.membership.get_canister_http_committee(height) { - Ok(members) => ic_types::consensus::get_faults_tolerated(members.len()), - _ => { - warn!(self.log, "Failed to get canister http committee"); - return validation_failed(CanisterHttpPayloadValidationFailure::Membership); - } - }; + // Defer signature verification of the reconstructed response shares. + sig_inputs.extend(response_share_sig_inputs(&reconstructed_shares)); - // Accumulates the signature-verification inputs of every flexible / - // divergence response share in the payload, so that all of them can be - // checked in a single batched multi-message verification call at the - // very end. - let mut sig_inputs: Vec = Vec::new(); + let faults_tolerated = ic_types::consensus::get_faults_tolerated(committee.len()); for response in &payload.divergence_responses { let (valid_signers, invalid_signers): (Vec, Vec) = response @@ -621,6 +575,16 @@ impl CanisterHttpPayloadBuilderImpl { InvalidCanisterHttpPayloadReason::InvalidPayloadSection(callback_id), ); } + + // Enforce per-replica refund allowance for divergence shares. + for share in grouped_shares.values().flatten() { + utils::check_refund_allowance( + &share.content.payment_receipt, + context.refund_status.per_replica_allowance, + ) + .map_err(CanisterHttpPayloadValidationError::InvalidArtifact)?; + } + if !grouped_shares_meet_divergence_criteria(&grouped_shares, faults_tolerated) { return invalid_artifact( InvalidCanisterHttpPayloadReason::DivergenceProofDoesNotMeetDivergenceCriteria, @@ -679,6 +643,7 @@ impl CanisterHttpPayloadBuilderImpl { flex_committee, &mut seen_signers, consensus_registry_version, + context.refund_status.per_replica_allowance, ) .map_err(CanisterHttpPayloadValidationError::InvalidArtifact)?; @@ -745,6 +710,7 @@ impl CanisterHttpPayloadBuilderImpl { flex_committee, &mut seen_signers, consensus_registry_version, + context.refund_status.per_replica_allowance, ) .map_err(CanisterHttpPayloadValidationError::InvalidArtifact)?; @@ -809,6 +775,7 @@ impl CanisterHttpPayloadBuilderImpl { flex_committee, &mut seen_signers, consensus_registry_version, + context.refund_status.per_replica_allowance, ) .map_err(CanisterHttpPayloadValidationError::InvalidArtifact)?; } @@ -821,11 +788,11 @@ impl CanisterHttpPayloadBuilderImpl { let mut ok_entry_sizes: Vec = all_seen_shares .iter() - .filter(|share| !share.content.is_reject) + .filter(|share| !share.content.is_reject()) .map(|share| { FlexibleCanisterHttpResponseWithProof::count_bytes_from_parts( &context.request.sender, - share.content.content_size as usize, + share.content.content_size() as usize, share, ) }) @@ -958,7 +925,7 @@ impl IntoMessages<(Vec, CanisterHttpBatchStats)> .expect("Failed to parse a payload that was already validated"); let responses = messages.responses.into_iter().map(|response| { - if response.proof.signature.signatures_map.len() == 1 { + if response.proof.signatures.len() == 1 { stats.single_signature_responses += 1; } stats.responses += 1; @@ -1114,7 +1081,7 @@ fn flexible_error_into_consensus_response( } => { let num_ok = all_seen_shares .iter() - .filter(|s| !s.content.is_reject) + .filter(|s| !s.content.is_reject()) .count() as u32; let num_reject = all_seen_shares.len() as u32 - num_ok; let num_unseen = total_requests.saturating_sub(all_seen_shares.len() as u32); @@ -1123,7 +1090,7 @@ fn flexible_error_into_consensus_response( let node_details: Vec<_> = all_seen_shares .iter() .map(|share| { - let code = if share.content.is_reject { + let code = if share.content.is_reject() { "reject" } else { "ok" @@ -1133,7 +1100,7 @@ fn flexible_error_into_consensus_response( report: HttpRequestResourceReport::default(), error: Some(FlexibleHttpNodeError { code: code.to_string(), - message: format!("{} bytes", share.content.content_size), + message: format!("{} bytes", share.content.content_size()), }), } }) @@ -1141,8 +1108,8 @@ fn flexible_error_into_consensus_response( let mut ok_sizes: Vec<_> = all_seen_shares .iter() - .filter(|s| !s.content.is_reject) - .map(|share| share.content.content_size) + .filter(|s| !s.content.is_reject()) + .map(|share| share.content.content_size()) .collect(); // Sort defensively, as validator doesn't enforce ordering on `all_seen_shares` ok_sizes.sort_unstable(); @@ -1187,7 +1154,7 @@ fn flexible_error_into_consensus_response( fn divergence_response_into_reject( response: CanisterHttpResponseDivergence, ) -> Option { - let Some(id) = response.shares.first().map(|share| share.content.id) else { + let Some(id) = response.shares.first().map(|share| share.content.id()) else { // NOTE: We skip delivering the divergence response, if it has no shares // Such a divergence response should never validate, therefore this should never happen // However, if it where ever to happen, we can ignore it here. @@ -1200,7 +1167,7 @@ fn divergence_response_into_reject( response .shares .into_iter() - .map(|share| share.content.content_hash.get().0) + .map(|share| share.content.metadata.content_hash.get().0) .for_each(|hash| { hash_counts .entry(hash) diff --git a/rs/https_outcalls/consensus/src/payload_builder/tests.rs b/rs/https_outcalls/consensus/src/payload_builder/tests.rs index bd750b5aa899..2e959e90a5e6 100644 --- a/rs/https_outcalls/consensus/src/payload_builder/tests.rs +++ b/rs/https_outcalls/consensus/src/payload_builder/tests.rs @@ -32,6 +32,7 @@ use ic_management_canister_types_private::{ use ic_metrics::MetricsRegistry; use ic_registry_subnet_features::SubnetFeatures; use ic_test_utilities::state_manager::RefMockStateManager; +use ic_test_utilities_consensus::fake::FakeContentSigner; use ic_test_utilities_registry::SubnetRecordBuilder; use ic_test_utilities_types::{ ids::{canister_test_id, node_id_to_u64, node_test_id, subnet_test_id}, @@ -45,18 +46,20 @@ use ic_types::{ }, canister_http::{ CANISTER_HTTP_MAX_RESPONSES_PER_BLOCK, CANISTER_HTTP_TIMEOUT_INTERVAL, CanisterHttpMethod, - CanisterHttpReject, CanisterHttpRequestContext, CanisterHttpResponse, - CanisterHttpResponseArtifact, CanisterHttpResponseContent, CanisterHttpResponseDivergence, - CanisterHttpResponseMetadata, CanisterHttpResponseShare, CanisterHttpResponseWithConsensus, - Replication, + CanisterHttpPaymentReceipt, CanisterHttpReject, CanisterHttpRequestContext, + CanisterHttpResponse, CanisterHttpResponseArtifact, CanisterHttpResponseContent, + CanisterHttpResponseDivergence, CanisterHttpResponseMetadata, CanisterHttpResponseProof, + CanisterHttpResponseReceipt, CanisterHttpResponseShare, CanisterHttpResponseSignature, + CanisterHttpResponseWithConsensus, Replication, }, consensus::get_faults_tolerated, crypto::{BasicSig, BasicSigOf, CryptoHash, CryptoHashOf, Signed, crypto_hash}, messages::{CallbackId, Payload, RejectContext}, registry::RegistryClientError, - signature::{BasicSignature, BasicSignatureBatch}, + signature::BasicSignature, time::UNIX_EPOCH, }; +use ic_types_cycles::Cycles; use rand::Rng; use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; use std::{ @@ -202,11 +205,9 @@ fn multiple_payload_test() { let past_payload = CanisterHttpPayload { responses: vec![CanisterHttpResponseWithConsensus { content: past_response, - proof: Signed { - content: past_metadata, - signature: BasicSignatureBatch { - signatures_map: BTreeMap::new(), - }, + proof: CanisterHttpResponseProof { + metadata: past_metadata, + signatures: BTreeMap::new(), }, }], timeouts: vec![], @@ -782,7 +783,10 @@ fn divergence_error_message() { sample.content_hash = CryptoHashOf::from(CryptoHash(new_hash.to_vec())); Signed { - content: sample, + content: CanisterHttpResponseReceipt { + metadata: sample, + payment_receipt: CanisterHttpPaymentReceipt::default(), + }, signature: BasicSignature { signature: BasicSigOf::new(BasicSig(vec![])), signer: node_test_id(node_id), @@ -877,15 +881,12 @@ fn non_replicated_request_response_coming_in_gossip_payload_created() { // The response must contain one signature. let proof = &parsed_payload.responses[0].proof; assert_eq!( - proof.signature.signatures_map.len(), + proof.signatures.len(), 1, "Proof should contain exactly one signature" ); assert!( - proof - .signature - .signatures_map - .contains_key(&delegated_node_id), + proof.signatures.contains_key(&delegated_node_id), "The single signature must be from the delegated node" ); }); @@ -950,15 +951,12 @@ fn non_replicated_request_with_extra_share_includes_only_delegated_share() { // The response must contain EXACTLY ONE signature, proving the "extra" share was ignored. let proof = &parsed_payload.responses[0].proof; assert_eq!( - proof.signature.signatures_map.len(), + proof.signatures.len(), 1, "Proof should contain exactly one signature" ); assert!( - proof - .signature - .signatures_map - .contains_key(&delegated_node_id), + proof.signatures.contains_key(&delegated_node_id), "The single signature must be from the delegated node" ); }); @@ -1050,11 +1048,7 @@ fn validate_payload_succeeds_for_valid_non_replicated_response() { let (response, metadata) = test_response_and_metadata(callback_id.get()); let mut proof = response_and_metadata_to_proof(&response, &metadata); // The proof must contain exactly ONE signature, from the DELEGATED node. - proof - .proof - .signature - .signatures_map - .insert(delegated_node_id, BasicSigOf::new(BasicSig(vec![]))); + add_signer_to_proof(&mut proof, delegated_node_id); let payload = CanisterHttpPayload { responses: vec![proof], @@ -1074,6 +1068,159 @@ fn validate_payload_succeeds_for_valid_non_replicated_response() { }); } +#[test] +fn validate_payload_fails_for_refund_exceeding_allowance_non_replicated() { + let delegated_node_id = node_test_id(1); + let callback_id = CallbackId::from(99); + + let (response, metadata) = test_response_and_metadata(callback_id.get()); + let mut proof = response_and_metadata_to_proof(&response, &metadata); + add_signer_with_excess_refund_to_proof(&mut proof, delegated_node_id); + + assert_payload_rejected_for_excess_refund( + 4, + vec![( + callback_id, + request_context(Replication::NonReplicated(delegated_node_id)), + )], + default_validation_context(), + CanisterHttpPayload { + responses: vec![proof], + ..Default::default() + }, + ); +} + +#[test] +fn validate_payload_fails_for_refund_exceeding_allowance_fully_replicated() { + let num_nodes = 4; + let callback_id = CallbackId::from(99); + + let (response, metadata) = test_response_and_metadata(callback_id.get()); + let mut proof = response_and_metadata_to_proof(&response, &metadata); + for node in 0..num_nodes as u64 { + add_signer_with_excess_refund_to_proof(&mut proof, node_test_id(node)); + } + + assert_payload_rejected_for_excess_refund( + num_nodes, + vec![(callback_id, request_context(Replication::FullyReplicated))], + default_validation_context(), + CanisterHttpPayload { + responses: vec![proof], + ..Default::default() + }, + ); +} + +#[test] +fn validate_payload_fails_for_refund_exceeding_allowance_divergence() { + let callback_id = CallbackId::from(99); + + let (_response, metadata) = test_response_and_metadata(callback_id.get()); + let payload = CanisterHttpPayload { + divergence_responses: vec![CanisterHttpResponseDivergence { + shares: vec![share_with_excess_refund(0, &metadata)], + }], + ..Default::default() + }; + + assert_payload_rejected_for_excess_refund( + 4, + vec![(callback_id, request_context(Replication::FullyReplicated))], + default_validation_context(), + payload, + ); +} + +#[test] +fn validate_payload_fails_for_refund_exceeding_allowance_flexible_response() { + let num_nodes = 4; + let committee: BTreeSet<_> = (0..num_nodes as u64).map(node_test_id).collect(); + let callback_id = CallbackId::from(99); + + let (response, metadata) = test_response_and_metadata_with_content( + callback_id.get(), + CanisterHttpResponseContent::Success(b"flexible".to_vec()), + ); + let payload = CanisterHttpPayload { + flexible_responses: vec![FlexibleCanisterHttpResponses { + callback_id, + responses: vec![FlexibleCanisterHttpResponseWithProof { + response, + proof: share_with_excess_refund(0, &metadata), + }], + }], + ..Default::default() + }; + + assert_payload_rejected_for_excess_refund( + num_nodes, + vec![(callback_id, flexible_request_context(committee, 1, 4))], + default_validation_context(), + payload, + ); +} + +#[test] +fn validate_payload_fails_for_refund_exceeding_allowance_too_many_rejects() { + let num_nodes = 4; + let committee: BTreeSet<_> = (0..num_nodes as u64).map(node_test_id).collect(); + let callback_id = CallbackId::from(99); + + let (response, metadata) = test_response_and_metadata_with_content( + callback_id.get(), + CanisterHttpResponseContent::Reject(CanisterHttpReject { + reject_code: RejectCode::SysTransient, + message: "could not connect".to_string(), + }), + ); + let payload = CanisterHttpPayload { + flexible_errors: vec![FlexibleCanisterHttpError::TooManyRejects { + callback_id, + reject_responses: vec![FlexibleCanisterHttpResponseWithProof { + response, + proof: share_with_excess_refund(0, &metadata), + }], + }], + ..Default::default() + }; + + assert_payload_rejected_for_excess_refund( + num_nodes, + vec![(callback_id, flexible_request_context(committee, 1, 4))], + default_validation_context(), + payload, + ); +} + +#[test] +fn validate_payload_fails_for_refund_exceeding_allowance_responses_too_large() { + let num_nodes = 4; + let committee: BTreeSet<_> = (0..num_nodes as u64).map(node_test_id).collect(); + let callback_id = CallbackId::from(99); + + let (_response, metadata) = test_response_and_metadata(callback_id.get()); + let payload = CanisterHttpPayload { + flexible_errors: vec![FlexibleCanisterHttpError::ResponsesTooLarge { + callback_id, + all_seen_shares: vec![share_with_excess_refund(0, &metadata)], + // Match the committee size and context `min_responses` so validation + // reaches the per-share refund check. + total_requests: num_nodes as u32, + min_responses: 2, + }], + ..Default::default() + }; + + assert_payload_rejected_for_excess_refund( + num_nodes, + vec![(callback_id, flexible_request_context(committee, 2, 4))], + default_validation_context(), + payload, + ); +} + #[test] fn validate_payload_fails_for_non_replicated_response_with_wrong_signer() { // ARRANGE @@ -1105,10 +1252,8 @@ fn validate_payload_fails_for_non_replicated_response_with_wrong_signer() { let (response, metadata) = test_response_and_metadata(callback_id.get()); let mut proof = response_and_metadata_to_proof(&response, &metadata); - proof.proof.signature.signatures_map.insert( - wrong_signer_node_id, // The illegal signature - BasicSigOf::new(BasicSig(vec![])), - ); + // The illegal signature from a non-member node. + add_signer_to_proof(&mut proof, wrong_signer_node_id); let payload = CanisterHttpPayload { responses: vec![proof], @@ -1175,7 +1320,7 @@ fn validate_payload_fails_for_response_with_no_signatures() { let mut proof = response_and_metadata_to_proof(&response, &metadata); // Ensure the signature map is empty. - proof.proof.signature.signatures_map = BTreeMap::new(); + proof.proof.signatures = BTreeMap::new(); let payload = CanisterHttpPayload { responses: vec![proof], @@ -1251,11 +1396,7 @@ fn validate_payload_fails_when_non_replicated_proof_is_for_fully_replicated_requ let mut proof = response_and_metadata_to_proof(&response, &metadata); // The proof only contains one signature. - proof - .proof - .signature - .signatures_map - .insert(signer_node_id, BasicSigOf::new(BasicSig(vec![]))); + add_signer_to_proof(&mut proof, signer_node_id); let payload = CanisterHttpPayload { responses: vec![proof], @@ -1331,11 +1472,7 @@ fn validate_payload_fails_for_duplicate_non_replicated_response() { // 3. Craft a valid proof for the NonReplicated response. let (response, metadata) = test_response_and_metadata(duplicate_callback_id.get()); let mut proof = response_and_metadata_to_proof(&response, &metadata); - proof - .proof - .signature - .signatures_map - .insert(delegated_node_id, BasicSigOf::new(BasicSig(vec![]))); + add_signer_to_proof(&mut proof, delegated_node_id); // 4. Create a payload that includes this same proof twice. let payload = CanisterHttpPayload { @@ -1475,31 +1612,118 @@ pub(crate) fn metadata_to_share_with_signature( metadata: &CanisterHttpResponseMetadata, signature: Vec, ) -> CanisterHttpResponseShare { + let signer = node_test_id(from_node); Signed { - content: metadata.clone(), + content: CanisterHttpResponseReceipt { + metadata: metadata.clone(), + payment_receipt: CanisterHttpPaymentReceipt::default(), + }, signature: BasicSignature { signature: BasicSigOf::new(BasicSig(signature)), - signer: node_test_id(from_node), + signer, }, } } -/// Creates a [`CanisterHttpResponseWithConsensus`] from a [`CanisterHttpResponse`] and [`CanisterHttpResponseMetadata`] +/// Creates a [`CanisterHttpResponseWithConsensus`] from a [`CanisterHttpResponse`] and [`CanisterHttpResponseMetadata`]. +/// +/// The proof starts out with an empty signatures map; tests insert +/// per-signer receipt + signature pairs via [`add_signer_to_proof`]. pub(crate) fn response_and_metadata_to_proof( response: &CanisterHttpResponse, metadata: &CanisterHttpResponseMetadata, ) -> CanisterHttpResponseWithConsensus { CanisterHttpResponseWithConsensus { content: response.clone(), - proof: Signed { - content: metadata.clone(), - signature: BasicSignatureBatch { - signatures_map: BTreeMap::new(), - }, + proof: CanisterHttpResponseProof { + metadata: metadata.clone(), + signatures: BTreeMap::new(), }, } } +/// Inserts a fake signature together with a default payment receipt for +/// `signer` into an aggregated proof. +pub(crate) fn add_signer_to_proof(proof: &mut CanisterHttpResponseWithConsensus, signer: NodeId) { + proof.proof.signatures.insert( + signer, + CanisterHttpResponseSignature { + payment_receipt: CanisterHttpPaymentReceipt::default(), + signature: BasicSigOf::new(BasicSig(vec![])), + }, + ); +} + +/// A payment receipt whose refund exceeds the default (zero) per-replica +/// allowance. +fn receipt_exceeding_allowance() -> CanisterHttpPaymentReceipt { + CanisterHttpPaymentReceipt { + refund: Cycles::new(1), + } +} + +/// Builds a share for `metadata` signed by `signer_node` whose payment receipt +/// claims a refund exceeding the default per-replica allowance. +fn share_with_excess_refund( + signer_node: u64, + metadata: &CanisterHttpResponseMetadata, +) -> CanisterHttpResponseShare { + CanisterHttpResponseShare::fake( + CanisterHttpResponseReceipt { + metadata: metadata.clone(), + payment_receipt: receipt_exceeding_allowance(), + }, + node_test_id(signer_node), + ) +} + +/// Inserts a fake signature for `signer` together with a payment receipt whose +/// refund exceeds the default per-replica allowance into an aggregated proof. +fn add_signer_with_excess_refund_to_proof( + proof: &mut CanisterHttpResponseWithConsensus, + signer: NodeId, +) { + proof.proof.signatures.insert( + signer, + CanisterHttpResponseSignature { + payment_receipt: receipt_exceeding_allowance(), + signature: BasicSigOf::new(BasicSig(vec![])), + }, + ); +} + +/// Configures a payload builder with `num_nodes` nodes and the given request +/// `contexts`, validates `payload`, and asserts that it is rejected because a +/// payment receipt's refund exceeds the per-replica allowance. +fn assert_payload_rejected_for_excess_refund( + num_nodes: usize, + contexts: Vec<(CallbackId, CanisterHttpRequestContext)>, + validation_context: ValidationContext, + payload: CanisterHttpPayload, +) { + test_config_with_http_feature(true, num_nodes, |mut payload_builder, _| { + inject_request_contexts(&mut payload_builder, contexts); + let validation_result = payload_builder.validate_payload( + Height::from(1), + &test_proposal_context(&validation_context), + &payload_to_bytes_max_4mb(payload), + &[], + ); + assert_matches!( + validation_result, + Err(ValidationError::InvalidArtifact( + InvalidPayloadReason::InvalidCanisterHttpPayload( + InvalidCanisterHttpPayloadReason::RefundExceedsAllowance { + refund, + per_replica_allowance, + }, + ), + )) if refund == receipt_exceeding_allowance().refund + && per_replica_allowance == Cycles::new(0) + ); + }); +} + /// Creates a vector of [`CanisterHttpResponseShare`]s by calling [`metadata_to_share`] pub(crate) fn metadata_to_shares( num_nodes: usize, @@ -2212,7 +2436,7 @@ fn flexible_invalid_callback_id_mismatch_in_proof() { setup_test_with_flexible_context(4, callback_id, committee, 1, 4, |payload_builder, _pool| { let mut entry = flexible_response(42, 0, b"data"); - entry.proof.content.id = mismatched_id; + entry.proof.content.metadata.id = mismatched_id; let payload = flexible_payload(vec![FlexibleCanisterHttpResponses { callback_id, @@ -2307,7 +2531,7 @@ fn flexible_invalid_content_hash_mismatch() { let mut entry = flexible_response(42, 0, b"data"); let expected_calculated_hash = crypto_hash(&entry.response); let wrong_metadata_hash = CryptoHashOf::new(CryptoHash(vec![0xff; 32])); - entry.proof.content.content_hash = wrong_metadata_hash.clone(); + entry.proof.content.metadata.content_hash = wrong_metadata_hash.clone(); let payload = flexible_payload(vec![FlexibleCanisterHttpResponses { callback_id, @@ -2342,8 +2566,8 @@ fn flexible_invalid_content_size_mismatch() { setup_test_with_flexible_context(4, callback_id, committee, 1, 4, |payload_builder, _pool| { let mut entry = flexible_response(42, 0, b"data"); let expected_size = entry.response.content.count_bytes() as u32; - entry.proof.content.content_size = expected_size.wrapping_add(1); - let wrong_size = entry.proof.content.content_size; + entry.proof.content.metadata.content_size = expected_size.wrapping_add(1); + let wrong_size = entry.proof.content.metadata.content_size; let payload = flexible_payload(vec![FlexibleCanisterHttpResponses { callback_id, @@ -2377,7 +2601,7 @@ fn flexible_invalid_is_reject_mismatch() { setup_test_with_flexible_context(4, callback_id, committee, 1, 4, |payload_builder, _pool| { let mut entry = flexible_response(42, 0, b"data"); - entry.proof.content.is_reject = !entry.proof.content.is_reject; + entry.proof.content.metadata.is_reject = !entry.proof.content.metadata.is_reject; let payload = flexible_payload(vec![FlexibleCanisterHttpResponses { callback_id, @@ -2410,11 +2634,7 @@ fn flexible_response_in_regular_section_rejected() { setup_test_with_flexible_context(4, callback_id, committee, 2, 4, |payload_builder, _pool| { let (response, metadata) = test_response_and_metadata(callback_id.get()); let mut proof = response_and_metadata_to_proof(&response, &metadata); - proof - .proof - .signature - .signatures_map - .insert(node_test_id(0), BasicSigOf::new(BasicSig(vec![]))); + add_signer_to_proof(&mut proof, node_test_id(0)); let payload = CanisterHttpPayload { responses: vec![proof], @@ -2583,7 +2803,7 @@ fn flexible_invalid_registry_version_mismatch() { setup_test_with_flexible_context(4, callback_id, committee, 1, 4, |payload_builder, _pool| { let wrong_registry_version = RegistryVersion::new(999); let mut entry = flexible_response(42, 0, b"data"); - entry.proof.content.registry_version = wrong_registry_version; + entry.proof.content.metadata.registry_version = wrong_registry_version; let validation_context = default_validation_context(); let expected_registry_version = validation_context.registry_version; @@ -3040,7 +3260,7 @@ fn flexible_build_responses_too_large() { } => { assert_eq!(*cb, callback_id); assert_eq!(all_seen_shares.len(), 4); - assert!(all_seen_shares.iter().all(|s| !s.content.is_reject)); + assert!(all_seen_shares.iter().all(|s| !s.content.is_reject())); assert_eq!(*total_requests, 4); assert_eq!(*min_responses, 2); } @@ -3133,8 +3353,8 @@ fn flexible_build_responses_too_large_with_rejects_reducing_unseen() { } => { assert_eq!(*cb, callback_id); assert_eq!(all_seen_shares.len(), 5); - let ok_count = all_seen_shares.iter().filter(|s| !s.content.is_reject).count(); - let reject_count = all_seen_shares.iter().filter(|s| s.content.is_reject).count(); + let ok_count = all_seen_shares.iter().filter(|s| !s.content.is_reject()).count(); + let reject_count = all_seen_shares.iter().filter(|s| s.content.is_reject()).count(); assert_eq!(ok_count, 3); assert_eq!(reject_count, 2); assert_eq!(*total_requests, 6); @@ -3195,8 +3415,8 @@ fn flexible_build_responses_too_large_fewer_ok_than_min_responses() { } => { assert_eq!(*cb, callback_id); assert_eq!(all_seen_shares.len(), 5); - let ok_count = all_seen_shares.iter().filter(|s| !s.content.is_reject).count(); - let reject_count = all_seen_shares.iter().filter(|s| s.content.is_reject).count(); + let ok_count = all_seen_shares.iter().filter(|s| !s.content.is_reject()).count(); + let reject_count = all_seen_shares.iter().filter(|s| s.content.is_reject()).count(); assert_eq!(ok_count, 3); assert_eq!(reject_count, 2); assert_eq!(*total_requests, 6); @@ -3984,7 +4204,7 @@ fn flexible_error_responses_too_large_registry_version_mismatch() { let share_ok = metadata_share_with_content_size(callback_id.get(), 0, huge); // Share with wrong registry version let mut share_bad = metadata_share_with_content_size(callback_id.get(), 1, huge); - share_bad.content.registry_version = RegistryVersion::new(999); + share_bad.content.metadata.registry_version = RegistryVersion::new(999); let payload = CanisterHttpPayload { flexible_errors: vec![FlexibleCanisterHttpError::ResponsesTooLarge { @@ -4269,7 +4489,7 @@ fn flexible_error_too_many_rejects_registry_version_mismatch() { setup_test_with_flexible_context(num_nodes, callback_id, committee, 3, 4, |pb, _pool| { let entry_ok = flexible_reject_response(callback_id.get(), 0); let mut entry_bad = flexible_reject_response(callback_id.get(), 1); - entry_bad.proof.content.registry_version = RegistryVersion::new(999); + entry_bad.proof.content.metadata.registry_version = RegistryVersion::new(999); let payload = CanisterHttpPayload { flexible_errors: vec![FlexibleCanisterHttpError::TooManyRejects { @@ -4304,7 +4524,8 @@ fn flexible_error_too_many_rejects_content_hash_mismatch() { setup_test_with_flexible_context(num_nodes, callback_id, committee, 3, 4, |pb, _pool| { let entry_ok = flexible_reject_response(callback_id.get(), 0); let mut entry_bad = flexible_reject_response(callback_id.get(), 1); - entry_bad.proof.content.content_hash = CryptoHashOf::new(CryptoHash(vec![0xFF; 32])); + entry_bad.proof.content.metadata.content_hash = + CryptoHashOf::new(CryptoHash(vec![0xFF; 32])); let payload = CanisterHttpPayload { flexible_errors: vec![FlexibleCanisterHttpError::TooManyRejects { @@ -4339,7 +4560,7 @@ fn flexible_error_too_many_rejects_content_size_mismatch() { setup_test_with_flexible_context(num_nodes, callback_id, committee, 3, 4, |pb, _pool| { let entry_ok = flexible_reject_response(callback_id.get(), 0); let mut entry_bad = flexible_reject_response(callback_id.get(), 1); - entry_bad.proof.content.content_size = 999_999; + entry_bad.proof.content.metadata.content_size = 999_999; let payload = CanisterHttpPayload { flexible_errors: vec![FlexibleCanisterHttpError::TooManyRejects { @@ -4374,7 +4595,7 @@ fn flexible_error_too_many_rejects_is_reject_mismatch() { setup_test_with_flexible_context(num_nodes, callback_id, committee, 3, 4, |pb, _pool| { let entry_ok = flexible_reject_response(callback_id.get(), 0); let mut entry_bad = flexible_reject_response(callback_id.get(), 1); - entry_bad.proof.content.is_reject = !entry_bad.proof.content.is_reject; + entry_bad.proof.content.metadata.is_reject = !entry_bad.proof.content.metadata.is_reject; let payload = CanisterHttpPayload { flexible_errors: vec![FlexibleCanisterHttpError::TooManyRejects { @@ -4410,7 +4631,7 @@ fn flexible_error_too_many_rejects_proof_id_mismatch() { let entry_ok = flexible_reject_response(callback_id.get(), 0); let mut entry_bad = flexible_reject_response(callback_id.get(), 1); // response.id stays correct, but proof.content.id is wrong - entry_bad.proof.content.id = CallbackId::new(999); + entry_bad.proof.content.metadata.id = CallbackId::new(999); let payload = CanisterHttpPayload { flexible_errors: vec![FlexibleCanisterHttpError::TooManyRejects { diff --git a/rs/https_outcalls/consensus/src/payload_builder/utils.rs b/rs/https_outcalls/consensus/src/payload_builder/utils.rs index 521c2e49b379..18b0635ce959 100644 --- a/rs/https_outcalls/consensus/src/payload_builder/utils.rs +++ b/rs/https_outcalls/consensus/src/payload_builder/utils.rs @@ -7,13 +7,16 @@ use ic_types::{ FlexibleCanisterHttpResponses, MAX_CANISTER_HTTP_PAYLOAD_SIZE, }, canister_http::{ - CanisterHttpResponse, CanisterHttpResponseContent, CanisterHttpResponseMetadata, - CanisterHttpResponseShare, CanisterHttpResponseWithConsensus, + CanisterHttpPaymentReceipt, CanisterHttpResponse, CanisterHttpResponseContent, + CanisterHttpResponseMetadata, CanisterHttpResponseProof, CanisterHttpResponseReceipt, + CanisterHttpResponseShare, CanisterHttpResponseSignature, + CanisterHttpResponseWithConsensus, }, - crypto::{BasicSigOf, crypto_hash}, + crypto::{BasicSigOf, Signed, crypto_hash}, messages::CallbackId, signature::BasicSignature, }; +use ic_types_cycles::Cycles; use std::{ collections::{BTreeMap, BTreeSet, HashSet}, mem::size_of, @@ -32,7 +35,7 @@ pub(crate) fn check_response_consistency( response: &CanisterHttpResponseWithConsensus, ) -> Result<(), InvalidCanisterHttpPayloadReason> { let content = &response.content; - let metadata = &response.proof.content; + let metadata = &response.proof.metadata; // Check metadata field consistency if metadata.id != content.id { @@ -72,6 +75,67 @@ pub(crate) fn check_response_consistency( Ok(()) } +/// Enforces the per-replica refund allowance from the request context: the +/// `refund` claimed in the payment receipt must never exceed the +/// `per_replica_allowance` derived from the request's context. +pub(crate) fn check_refund_allowance( + receipt: &CanisterHttpPaymentReceipt, + per_replica_allowance: Cycles, +) -> Result<(), InvalidCanisterHttpPayloadReason> { + if receipt.refund > per_replica_allowance { + return Err(InvalidCanisterHttpPayloadReason::RefundExceedsAllowance { + refund: receipt.refund, + per_replica_allowance, + }); + } + Ok(()) +} + +/// Reconstructs, for every signer of an aggregated proof, the +/// [`CanisterHttpResponseShare`] that signer actually signed: the shared +/// [`CanisterHttpResponseMetadata`] combined with that signer's own +/// [`CanisterHttpPaymentReceipt`], paired with that signer's basic signature. +pub(crate) fn reconstruct_individual_shares( + proof: &CanisterHttpResponseProof, +) -> impl Iterator + '_ { + proof.signatures.iter().map(|(signer, sig)| Signed { + content: CanisterHttpResponseReceipt { + metadata: proof.metadata.clone(), + payment_receipt: sig.payment_receipt.clone(), + }, + signature: BasicSignature { + signature: sig.signature.clone(), + signer: *signer, + }, + }) +} + +/// Assembles a [`CanisterHttpResponseProof`] from a slice of contributing +/// shares: the shared `metadata` together with, for each signer, the +/// basic signature and payment receipt taken directly from that signer's +/// share. +pub(crate) fn aggregate_shares( + metadata: CanisterHttpResponseMetadata, + shares: &[&CanisterHttpResponseShare], +) -> CanisterHttpResponseProof { + let signatures = shares + .iter() + .map(|share| { + ( + share.signature.signer, + CanisterHttpResponseSignature { + payment_receipt: share.content.payment_receipt.clone(), + signature: share.signature.signature.clone(), + }, + ) + }) + .collect(); + CanisterHttpResponseProof { + metadata, + signatures, + } +} + /// Validates a single [`FlexibleCanisterHttpResponseWithProof`]. /// /// Checks callback-id consistency, share validity (using @@ -86,6 +150,7 @@ pub(crate) fn validate_flexible_response_with_proof( flex_committee: &BTreeSet, seen_signers: &mut HashSet, consensus_registry_version: RegistryVersion, + per_replica_allowance: Cycles, ) -> Result<(), InvalidCanisterHttpPayloadReason> { if response_with_proof.response.id != callback_id { return Err( @@ -102,28 +167,29 @@ pub(crate) fn validate_flexible_response_with_proof( flex_committee, seen_signers, consensus_registry_version, + per_replica_allowance, )?; let calculated_hash = crypto_hash(&response_with_proof.response); - if calculated_hash != response_with_proof.proof.content.content_hash { + if &calculated_hash != response_with_proof.proof.content.content_hash() { return Err(InvalidCanisterHttpPayloadReason::ContentHashMismatch { - metadata_hash: response_with_proof.proof.content.content_hash.clone(), + metadata_hash: response_with_proof.proof.content.content_hash().clone(), calculated_hash, }); } let calculated_size = response_with_proof.response.content.count_bytes() as u32; - if calculated_size != response_with_proof.proof.content.content_size { + if calculated_size != response_with_proof.proof.content.content_size() { return Err(InvalidCanisterHttpPayloadReason::ContentSizeMismatch { - metadata_size: response_with_proof.proof.content.content_size, + metadata_size: response_with_proof.proof.content.content_size(), calculated_size, }); } let calculated_is_reject = response_with_proof.response.content.is_reject(); - if calculated_is_reject != response_with_proof.proof.content.is_reject { + if calculated_is_reject != response_with_proof.proof.content.is_reject() { return Err(InvalidCanisterHttpPayloadReason::IsRejectMismatch { - metadata_is_reject: response_with_proof.proof.content.is_reject, + metadata_is_reject: response_with_proof.proof.content.is_reject(), calculated_is_reject, }); } @@ -134,7 +200,7 @@ pub(crate) fn validate_flexible_response_with_proof( /// Validates a single [`CanisterHttpResponseShare`]'s metadata. /// /// Checks callback-id consistency, duplicate signers, committee membership, -/// and registry version. +/// registry version, and the per-replica refund allowance. /// /// **NOTE**: The signature is not verified. Callers are expected to /// batch-verify the signatures of all shares in the surrounding group via @@ -145,12 +211,15 @@ pub(crate) fn validate_response_share( flex_committee: &BTreeSet, seen_signers: &mut HashSet, consensus_registry_version: RegistryVersion, + per_replica_allowance: Cycles, ) -> Result<(), InvalidCanisterHttpPayloadReason> { - if share.content.id != callback_id { + check_refund_allowance(&share.content.payment_receipt, per_replica_allowance)?; + + if share.content.id() != callback_id { return Err( InvalidCanisterHttpPayloadReason::FlexibleCallbackIdMismatch { callback_id, - mismatched_id: share.content.id, + mismatched_id: share.content.id(), }, ); } @@ -171,10 +240,10 @@ pub(crate) fn validate_response_share( ); } - if share.content.registry_version != consensus_registry_version { + if share.content.registry_version() != consensus_registry_version { return Err(InvalidCanisterHttpPayloadReason::RegistryVersionMismatch { expected: consensus_registry_version, - received: share.content.registry_version, + received: share.content.registry_version(), }); } @@ -185,8 +254,8 @@ pub(crate) fn validate_response_share( /// [`BasicSigVerifier::verify_basic_sig_batch_multi_msg`]. pub(crate) type ResponseShareSigInput<'a> = ( NodeId, - &'a BasicSigOf, - &'a CanisterHttpResponseMetadata, + &'a BasicSigOf, + &'a CanisterHttpResponseReceipt, ); /// Maps response shares to the `(signer, signature, message)` inputs consumed by @@ -247,6 +316,13 @@ pub(crate) fn grouped_shares_meet_divergence_criteria( } } +/// Groups shares by callback id and then by their shared metadata. +/// +/// Shares from different replicas for the same outcall agree on the +/// shared metadata but each carry their own payment receipt. We key the +/// inner `BTreeMap` on the metadata (taken from +/// `share.content.metadata`), so each group holds all shares that voted +/// for the same response and differ only in their per-replica receipt. pub(crate) fn group_shares_by_callback_id< 'a, Shares: Iterator, @@ -259,9 +335,9 @@ pub(crate) fn group_shares_by_callback_id< BTreeMap>, > = BTreeMap::new(); for share in shares { - map.entry(share.content.id) + map.entry(share.content.id()) .or_default() - .entry(share.content.clone()) + .entry(share.content.metadata.clone()) .or_default() .push(share); } @@ -270,29 +346,23 @@ pub(crate) fn group_shares_by_callback_id< /// Finds a fully-replicated HTTP outcall response ready for consensus. /// -/// Iterates over response shares grouped by metadata, looking for one where -/// at least `threshold` distinct replicas produced the same response hash. -/// If found, returns the metadata, collected signatures, and response body. +/// Iterates over response shares grouped by metadata, looking for one +/// where at least `threshold` distinct replicas produced the same +/// response hash. If found, returns the assembled +/// [`CanisterHttpResponseWithConsensus`]. pub(crate) fn find_fully_replicated_response( grouped_shares: &BTreeMap>, threshold: usize, pool_access: &dyn CanisterHttpPool, -) -> Option<( - CanisterHttpResponseMetadata, - BTreeSet>, - CanisterHttpResponse, -)> { +) -> Option { grouped_shares.iter().find_map(|(metadata, shares)| { let signers: BTreeSet<_> = shares.iter().map(|share| share.signature.signer).collect(); if signers.len() >= threshold { pool_access .get_response_content_by_hash(&metadata.content_hash) - .map(|content| { - ( - metadata.clone(), - shares.iter().map(|share| share.signature.clone()).collect(), - content, - ) + .map(|content| CanisterHttpResponseWithConsensus { + content, + proof: aggregate_shares(metadata.clone(), shares), }) } else { None @@ -303,16 +373,12 @@ pub(crate) fn find_fully_replicated_response( /// Finds a non-replicated HTTP outcall response from the designated node. /// /// Looks through the grouped shares for one signed by `designated_node_id`. -/// If found, returns the metadata, the single signature, and response body. +/// If found, returns the assembled [`CanisterHttpResponseWithConsensus`]. pub(crate) fn find_non_replicated_response( grouped_shares: &BTreeMap>, designated_node_id: &NodeId, pool_access: &dyn CanisterHttpPool, -) -> Option<( - CanisterHttpResponseMetadata, - BTreeSet>, - CanisterHttpResponse, -)> { +) -> Option { grouped_shares.iter().find_map(|(metadata, shares)| { shares .iter() @@ -320,34 +386,14 @@ pub(crate) fn find_non_replicated_response( .and_then(|correct_share| { pool_access .get_response_content_by_hash(&metadata.content_hash) - .map(|content| { - ( - metadata.clone(), - BTreeSet::from([correct_share.signature.clone()]), - content, - ) + .map(|content| CanisterHttpResponseWithConsensus { + content, + proof: aggregate_shares(metadata.clone(), &[correct_share]), }) }) }) } -/// Estimates the byte size of a [`CanisterHttpResponseWithConsensus`] before -/// the proof has been aggregated. -/// -/// This function mirrors the implementation of -/// `CanisterHttpResponseWithConsensus::count_bytes()`: -/// proof.count_bytes() → metadata.count_bytes() + Σ share.count_bytes() -/// content.count_bytes() → content.count_bytes() -pub(crate) fn estimate_response_with_consensus_size( - metadata: &CanisterHttpResponseMetadata, - shares: &BTreeSet>, - content: &CanisterHttpResponse, -) -> usize { - metadata.count_bytes() - + shares.iter().map(|s| s.count_bytes()).sum::() - + content.count_bytes() -} - /// Result of scanning flexible HTTP outcall shares for a single callback. pub(crate) enum FlexibleFindResult { /// Collected enough OK responses for consensus. diff --git a/rs/https_outcalls/consensus/src/pool_manager.rs b/rs/https_outcalls/consensus/src/pool_manager.rs index eac24c4f2b6f..198105f8b807 100644 --- a/rs/https_outcalls/consensus/src/pool_manager.rs +++ b/rs/https_outcalls/consensus/src/pool_manager.rs @@ -167,7 +167,7 @@ impl CanisterHttpPoolManagerImpl { canister_http_pool .get_validated_shares() .filter_map(|share| { - if active_callback_ids.contains(&share.content.id) { + if active_callback_ids.contains(&share.content.id()) { None } else { Some(CanisterHttpChangeAction::RemoveValidated(share.clone())) @@ -177,10 +177,10 @@ impl CanisterHttpPoolManagerImpl { canister_http_pool .get_unvalidated_artifacts() // Only check the unvalidated shares belonging to the requests that we can validate. - .filter(|artifact| artifact.share.content.id < next_callback_id) + .filter(|artifact| artifact.share.content.id() < next_callback_id) .filter_map(|artifact| { let share = &artifact.share; - if active_callback_ids.contains(&share.content.id) { + if active_callback_ids.contains(&share.content.id()) { None } else { Some(CanisterHttpChangeAction::RemoveUnvalidated(share.clone())) @@ -270,7 +270,7 @@ impl CanisterHttpPoolManagerImpl { .get_validated_shares() .filter_map(|share| { if share.signature.signer == self.replica_config.node_id { - Some(share.content.id) + Some(share.content.id()) } else { None } @@ -344,7 +344,7 @@ impl CanisterHttpPoolManagerImpl { loop { match self.http_adapter_shim.lock().unwrap().try_receive() { Err(TryReceiveError::Empty) => break, - Ok((mut response, _payment_receipt)) => { + Ok((mut response, payment_receipt)) => { // Drop the response if its context is no longer present in the replicated state // (e.g. the request has timed out or has already been answered by enough other nodes). let Some(context) = active_contexts.get(&response.id) else { @@ -380,18 +380,21 @@ impl CanisterHttpPoolManagerImpl { .ellipsize(MAXIMUM_ALLOWED_ERROR_MESSAGE_BYTES, 90); } - let response_metadata = CanisterHttpResponseMetadata { - id: response.id, - registry_version, - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: response.content.is_reject(), - replica_version: ReplicaVersion::default(), + let receipt_share = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: response.id, + registry_version, + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: response.content.is_reject(), + replica_version: ReplicaVersion::default(), + }, + payment_receipt, }; let signature = if let Ok(signature) = self .crypto .sign( - &response_metadata, + &receipt_share, self.replica_config.node_id, registry_version, ) @@ -402,7 +405,7 @@ impl CanisterHttpPoolManagerImpl { continue; }; let share = Signed { - content: response_metadata, + content: receipt_share, signature, }; self.requested_id_cache.borrow_mut().remove(&response.id); @@ -469,7 +472,7 @@ impl CanisterHttpPoolManagerImpl { let next_callback_id = self.next_callback_id(); let key_from_share = - |share: &CanisterHttpResponseShare| (share.signature.signer, share.content.id); + |share: &CanisterHttpResponseShare| (share.signature.signer, share.content.id()); let mut existing_signed_requests: HashSet<_> = canister_http_pool .get_validated_shares() @@ -478,12 +481,12 @@ impl CanisterHttpPoolManagerImpl { canister_http_pool .get_unvalidated_artifacts() - .filter(|artifact| artifact.share.content.id < next_callback_id) + .filter(|artifact| artifact.share.content.id() < next_callback_id) .filter_map(|artifact| { let share = &artifact.share; if existing_signed_requests.contains(&key_from_share(share)) { - return match is_current_protocol_version(&share.content.replica_version) { + return match is_current_protocol_version(share.content.replica_version()) { true => Some(CanisterHttpChangeAction::HandleInvalid( share.clone(), "Redundant share".into(), @@ -492,10 +495,19 @@ impl CanisterHttpPoolManagerImpl { }; } - let Some(context) = active_contexts.get(&share.content.id) else { + let Some(context) = active_contexts.get(&share.content.id()) else { return Some(CanisterHttpChangeAction::RemoveUnvalidated(share.clone())); }; + // Invalidate shares whose refund exceeds what a single + // replica is allowed to claim. + if share.content.refund() > context.refund_status.per_replica_allowance { + return Some(CanisterHttpChangeAction::HandleInvalid( + share.clone(), + "Refund is greater than replica allowance".to_string(), + )); + } + match &context.replication { Replication::FullyReplicated => { if artifact.response.is_some() { @@ -521,21 +533,22 @@ impl CanisterHttpPoolManagerImpl { )); }; - if share.content.content_hash != ic_types::crypto::crypto_hash(response) { + if share.content.content_hash() != &ic_types::crypto::crypto_hash(response) + { return Some(CanisterHttpChangeAction::HandleInvalid( share.clone(), "Content hash does not match the response".to_string(), )); } - if share.content.content_size != response.content.count_bytes() as u32 { + if share.content.content_size() != response.content.count_bytes() as u32 { return Some(CanisterHttpChangeAction::HandleInvalid( share.clone(), "Content size does not match the response".to_string(), )); } - if share.content.is_reject != response.content.is_reject() { + if share.content.is_reject() != response.content.is_reject() { return Some(CanisterHttpChangeAction::HandleInvalid( share.clone(), "is_reject does not match the response content".to_string(), @@ -711,6 +724,7 @@ pub mod test { messages::CallbackId, time::UNIX_EPOCH, }; + use ic_types_cycles::Cycles; use mockall::predicate::*; use mockall::*; use std::{collections::BTreeMap, str::FromStr}; @@ -816,13 +830,16 @@ pub mod test { // Try to insert a share for request id 1 (while the next expected one is the // default value 0). { - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(1), - registry_version: RegistryVersion::from(1), - content_hash: CryptoHashOf::new(CryptoHash(vec![])), - content_size: 0, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(1), + registry_version: RegistryVersion::from(1), + content_hash: CryptoHashOf::new(CryptoHash(vec![])), + content_size: 0, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let signature = crypto @@ -913,13 +930,16 @@ pub mod test { )]))), )); - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(0), - registry_version: RegistryVersion::from(1), - content_hash: CryptoHashOf::new(CryptoHash(vec![])), - content_size: 0, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(0), + registry_version: RegistryVersion::from(1), + content_hash: CryptoHashOf::new(CryptoHash(vec![])), + content_size: 0, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let mut canister_http_pool = @@ -945,7 +965,7 @@ pub mod test { )]); // add an unvalidated copy of the share, that has an outdated version instead - share.content.replica_version = + share.content.metadata.replica_version = ReplicaVersion::from_str("outdated_version").unwrap(); let artifact = CanisterHttpResponseArtifact { @@ -1019,13 +1039,16 @@ pub mod test { )]))), )); - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(0), - registry_version: RegistryVersion::from(1), - content_hash: CryptoHashOf::new(CryptoHash(vec![])), - content_size: 0, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(0), + registry_version: RegistryVersion::from(1), + content_hash: CryptoHashOf::new(CryptoHash(vec![])), + content_size: 0, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let mut canister_http_pool = @@ -1147,13 +1170,16 @@ pub mod test { )); let response = empty_canister_http_response(0); - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(0), - registry_version: RegistryVersion::from(1), - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(0), + registry_version: RegistryVersion::from(1), + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let signature = crypto @@ -1215,7 +1241,8 @@ pub mod test { CanisterHttpPoolImpl::new(MetricsRegistry::new(), no_op_logger()); let mut bad_share = share.clone(); - bad_share.content.content_hash = CryptoHashOf::new(CryptoHash(vec![1, 2, 3])); + bad_share.content.metadata.content_hash = + CryptoHashOf::new(CryptoHash(vec![1, 2, 3])); let artifact_with_mismatched_hash = CanisterHttpResponseArtifact { share: bad_share, @@ -1247,7 +1274,8 @@ pub mod test { CanisterHttpPoolImpl::new(MetricsRegistry::new(), no_op_logger()); let mut bad_share = share.clone(); - bad_share.content.content_size = bad_share.content.content_size.wrapping_add(1); + bad_share.content.metadata.content_size = + bad_share.content.metadata.content_size.wrapping_add(1); let artifact_with_mismatched_size = CanisterHttpResponseArtifact { share: bad_share, @@ -1279,7 +1307,7 @@ pub mod test { CanisterHttpPoolImpl::new(MetricsRegistry::new(), no_op_logger()); let mut bad_share = share.clone(); - bad_share.content.is_reject = !bad_share.content.is_reject; + bad_share.content.metadata.is_reject = !bad_share.content.metadata.is_reject; let artifact_with_mismatched_is_reject = CanisterHttpResponseArtifact { share: bad_share, @@ -1344,13 +1372,16 @@ pub mod test { // 3. MALICIOUS ARTIFACT: Create a share that is signed by the `wrong_signer_id`. let response = empty_canister_http_response(callback_id.get()); - let response_metadata = CanisterHttpResponseMetadata { - id: callback_id, - registry_version: RegistryVersion::from(1), - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: callback_id, + registry_version: RegistryVersion::from(1), + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let share = Signed { content: response_metadata.clone(), @@ -1442,13 +1473,16 @@ pub mod test { )); let response = empty_canister_http_response(0); - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(0), - registry_version: RegistryVersion::from(1), - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(0), + registry_version: RegistryVersion::from(1), + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let signature = crypto @@ -1580,13 +1614,16 @@ pub mod test { content: CanisterHttpResponseContent::Success(response_body_too_large), }; - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(0), - registry_version: RegistryVersion::from(1), - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(0), + registry_version: RegistryVersion::from(1), + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let share = Signed { content: response_metadata.clone(), @@ -1646,13 +1683,16 @@ pub mod test { content: CanisterHttpResponseContent::Success(response_body_ok), }; - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(0), - registry_version: RegistryVersion::from(1), - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(0), + registry_version: RegistryVersion::from(1), + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let share = Signed { content: response_metadata.clone(), @@ -1750,13 +1790,16 @@ pub mod test { ..empty_canister_http_response(0) }; - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(0), - registry_version: RegistryVersion::from(1), - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: true, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(0), + registry_version: RegistryVersion::from(1), + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: true, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let share = Signed { @@ -1904,7 +1947,7 @@ pub mod test { // Assert that the hash in the share matches the hash of the *truncated* response. let expected_hash = ic_types::crypto::crypto_hash(response); assert_eq!( - share.content.content_hash, expected_hash, + share.content.content_hash(), &expected_hash, "The share's content hash must match the pruned response" ); } @@ -2036,7 +2079,7 @@ pub mod test { // Assert that the hash in the share matches the hash of the now-pruned response. let expected_hash = ic_types::crypto::crypto_hash(response); assert_eq!( - share.content.content_hash, expected_hash, + share.content.content_hash(), &expected_hash, "The share's content hash must match the pruned response" ); } @@ -2092,13 +2135,16 @@ pub mod test { }; let dishonest_hash = ic_types::crypto::crypto_hash(&dishonest_response); - let response_metadata = CanisterHttpResponseMetadata { - id: callback_id, - registry_version: RegistryVersion::from(1), - content_hash: dishonest_hash, - content_size: dishonest_response.content.count_bytes() as u32, - is_reject: true, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: callback_id, + registry_version: RegistryVersion::from(1), + content_hash: dishonest_hash, + content_size: dishonest_response.content.count_bytes() as u32, + is_reject: true, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let share = Signed { content: response_metadata.clone(), @@ -2231,13 +2277,16 @@ pub mod test { ..empty_canister_http_response(0) }; - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(0), - registry_version: RegistryVersion::from(1), - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: true, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(0), + registry_version: RegistryVersion::from(1), + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: true, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let share = Signed { @@ -2309,13 +2358,16 @@ pub mod test { )]))), )); - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(7), - registry_version: RegistryVersion::from(1), - content_hash: CryptoHashOf::new(CryptoHash(vec![])), - content_size: 0, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(7), + registry_version: RegistryVersion::from(1), + content_hash: CryptoHashOf::new(CryptoHash(vec![])), + content_size: 0, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let signature = crypto @@ -2532,7 +2584,7 @@ pub mod test { assert_matches!( &change_set[0], CanisterHttpChangeAction::AddToValidated(share, response) => { - assert_eq!(share.content.id, active_callback_id); + assert_eq!(share.content.id(), active_callback_id); assert_eq!(response.id, active_callback_id); } ); @@ -2628,12 +2680,12 @@ pub mod test { let expected_response = empty_canister_http_response(callback_id.get()); assert_eq!(*response, expected_response); - assert_eq!(share.content.id, callback_id); + assert_eq!(share.content.id(), callback_id); assert_eq!( - share.content.content_hash, - ic_types::crypto::crypto_hash(&expected_response) + share.content.content_hash(), + &ic_types::crypto::crypto_hash(&expected_response) ); - assert_eq!(share.content.registry_version, RegistryVersion::from(1)); + assert_eq!(share.content.registry_version(), RegistryVersion::from(1)); assert_eq!(share.signature.signer, replica_config.node_id); } else { panic!( @@ -2710,13 +2762,16 @@ pub mod test { let change_set = pool_manager.generate_change_set(&canister_http_pool); assert_eq!(change_set.len(), 0); - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(7), - registry_version: RegistryVersion::from(1), - content_hash: CryptoHashOf::new(CryptoHash(vec![])), - content_size: 0, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(7), + registry_version: RegistryVersion::from(1), + content_hash: CryptoHashOf::new(CryptoHash(vec![])), + content_size: 0, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let signature = crypto @@ -2874,13 +2929,16 @@ pub mod test { // 3. MALICIOUS ARTIFACT: Create a share that is signed by the `wrong_signer_id`. let response = empty_canister_http_response(callback_id.get()); - let response_metadata = CanisterHttpResponseMetadata { - id: callback_id, - registry_version: RegistryVersion::from(1), - content_hash: crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: callback_id, + registry_version: RegistryVersion::from(1), + content_hash: crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let share = Signed { content: response_metadata.clone(), @@ -2973,13 +3031,16 @@ pub mod test { )); let response = empty_canister_http_response(callback_id.get()); - let response_metadata = CanisterHttpResponseMetadata { - id: callback_id, - registry_version: RegistryVersion::from(1), - content_hash: crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: callback_id, + registry_version: RegistryVersion::from(1), + content_hash: crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let signature = crypto @@ -3046,7 +3107,8 @@ pub mod test { CanisterHttpPoolImpl::new(MetricsRegistry::new(), no_op_logger()); let mut bad_share = share.clone(); - bad_share.content.content_hash = CryptoHashOf::new(CryptoHash(vec![1, 2, 3])); + bad_share.content.metadata.content_hash = + CryptoHashOf::new(CryptoHash(vec![1, 2, 3])); canister_http_pool.insert(UnvalidatedArtifact { message: CanisterHttpResponseArtifact { @@ -3077,7 +3139,8 @@ pub mod test { CanisterHttpPoolImpl::new(MetricsRegistry::new(), no_op_logger()); let mut bad_share = share.clone(); - bad_share.content.content_size = bad_share.content.content_size.wrapping_add(1); + bad_share.content.metadata.content_size = + bad_share.content.metadata.content_size.wrapping_add(1); canister_http_pool.insert(UnvalidatedArtifact { message: CanisterHttpResponseArtifact { @@ -3108,7 +3171,7 @@ pub mod test { CanisterHttpPoolImpl::new(MetricsRegistry::new(), no_op_logger()); let mut bad_share = share.clone(); - bad_share.content.is_reject = !bad_share.content.is_reject; + bad_share.content.metadata.is_reject = !bad_share.content.metadata.is_reject; canister_http_pool.insert(UnvalidatedArtifact { message: CanisterHttpResponseArtifact { @@ -3206,13 +3269,16 @@ pub mod test { content: CanisterHttpResponseContent::Success(vec![0; oversized_len]), }; - let response_metadata = CanisterHttpResponseMetadata { - id: callback_id, - registry_version: RegistryVersion::from(1), - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: callback_id, + registry_version: RegistryVersion::from(1), + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let share = Signed { content: response_metadata.clone(), @@ -3270,13 +3336,16 @@ pub mod test { ]), }; - let response_metadata = CanisterHttpResponseMetadata { - id: callback_id, - registry_version: RegistryVersion::from(1), - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: response.content.count_bytes() as u32, - is_reject: false, - replica_version: ReplicaVersion::default(), + let response_metadata = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: callback_id, + registry_version: RegistryVersion::from(1), + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: response.content.count_bytes() as u32, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let share = Signed { content: response_metadata.clone(), @@ -3389,15 +3458,121 @@ pub mod test { CanisterHttpChangeAction::AddToValidatedAndGossipResponse(share, response) => { let expected_response = empty_response; assert_eq!(*response, expected_response); - assert_eq!(share.content.id, callback_id); + assert_eq!(share.content.id(), callback_id); assert_eq!(share.signature.signer, replica_config.node_id); assert_eq!( - share.content.content_hash, - crypto_hash(&expected_response) + share.content.content_hash(), + &crypto_hash(&expected_response) ); } ); }); }); } + + #[test] + fn test_refund_greater_than_replica_allowance_is_invalid() { + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + with_test_replica_logger(|log| { + let Dependencies { + pool, + replica_config, + crypto, + state_manager, + registry, + .. + } = dependencies(pool_config.clone(), 5); + + // Use a context with a small per-replica allowance. + let request = CanisterHttpRequestContext { + refund_status: RefundStatus { + refundable_cycles: Cycles::new(1000), + per_replica_allowance: Cycles::new(100), + refunded_cycles: Cycles::new(0), + refunding_nodes: BTreeSet::new(), + }, + ..test_request_context( + Replication::FullyReplicated, + PricingVersion::Legacy, + None, + ) + }; + + state_manager + .get_mut() + .expect_get_latest_state() + .return_const(Labeled::new( + Height::from(1), + Arc::new(state_with_pending_http_calls(BTreeMap::from([( + CallbackId::from(0), + request, + )]))), + )); + + let mut canister_http_pool = + CanisterHttpPoolImpl::new(MetricsRegistry::new(), no_op_logger()); + + // Build a per-replica receipt share whose refund claim is + // larger than the per-replica allowance. + let receipt_share = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(0), + registry_version: RegistryVersion::from(1), + content_hash: CryptoHashOf::new(CryptoHash(vec![])), + content_size: 0, + is_reject: false, + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt { + refund: Cycles::new(200), + }, + }; + let signature = crypto + .sign( + &receipt_share, + replica_config.node_id, + RegistryVersion::from(1), + ) + .unwrap(); + let share = Signed { + content: receipt_share, + signature, + }; + + canister_http_pool.insert(UnvalidatedArtifact { + message: CanisterHttpResponseArtifact { + share, + response: None, + }, + peer_id: replica_config.node_id, + timestamp: UNIX_EPOCH, + }); + + let pool_manager = CanisterHttpPoolManagerImpl::new( + state_manager as Arc<_>, + Arc::new(Mutex::new(Box::new(MockNonBlockingChannel::new()))), + crypto, + pool.get_cache(), + replica_config, + SubnetType::Application, + Arc::clone(®istry) as Arc<_>, + MetricsRegistry::new(), + log, + ); + + let changes = pool_manager.validate_shares( + pool.get_cache().as_ref(), + &canister_http_pool, + Height::from(0), + ); + + assert_eq!(changes.len(), 1); + assert_matches!( + &changes[0], + CanisterHttpChangeAction::HandleInvalid(_, reason) + if reason == "Refund is greater than replica allowance" + ); + }) + }); + } } diff --git a/rs/interfaces/mocks/src/crypto.rs b/rs/interfaces/mocks/src/crypto.rs index 9673378ebd5b..e8b2f530f501 100644 --- a/rs/interfaces/mocks/src/crypto.rs +++ b/rs/interfaces/mocks/src/crypto.rs @@ -25,7 +25,7 @@ use ic_interfaces::crypto::{ ThresholdEcdsaSigner, ThresholdSchnorrSigVerifier, ThresholdSchnorrSigner, ThresholdSigVerifier, ThresholdSigVerifierByPublicKey, ThresholdSigner, VetKdProtocol, }; -use ic_types::canister_http::CanisterHttpResponseMetadata; +use ic_types::canister_http::CanisterHttpResponseReceipt; use ic_types::consensus::{ BlockMetadata, CatchUpContent, CatchUpContentProtobufBytes, FinalizationContent, NotarizationContent, RandomBeaconContent, RandomTapeContent, @@ -299,8 +299,8 @@ mockall::mock! { ) -> CryptoResult>; pub fn sign_basic_http( - &self, message: &CanisterHttpResponseMetadata, - ) -> CryptoResult>; + &self, message: &CanisterHttpResponseReceipt, + ) -> CryptoResult>; pub fn sign_basic_query( &self, message: &QueryResponseHash, @@ -457,24 +457,24 @@ mockall::mock! { registry_version: RegistryVersion, ) -> CryptoResult<()>; - // CanisterHttpResponseMetadata + // CanisterHttpResponseReceipt pub fn verify_basic_sig_http( &self, - signature: &BasicSigOf, - message: &CanisterHttpResponseMetadata, signer: NodeId, + signature: &BasicSigOf, + message: &CanisterHttpResponseReceipt, signer: NodeId, registry_version: RegistryVersion, ) -> CryptoResult<()>; pub fn combine_basic_sig_http( &self, - signatures: BTreeMap>, + signatures: BTreeMap>, registry_version: RegistryVersion, - ) -> CryptoResult>; + ) -> CryptoResult>; pub fn verify_basic_sig_batch_http( &self, - signature_batch: &BasicSignatureBatch, - message: &CanisterHttpResponseMetadata, + signature_batch: &BasicSignatureBatch, + message: &CanisterHttpResponseReceipt, registry_version: RegistryVersion, ) -> CryptoResult<()>; @@ -482,8 +482,8 @@ mockall::mock! { &self, inputs: Vec<( NodeId, - BasicSigOf, - CanisterHttpResponseMetadata, + BasicSigOf, + CanisterHttpResponseReceipt, )>, registry_version: RegistryVersion, ) -> CryptoResult<()>; @@ -783,7 +783,7 @@ impl_basic_signer!(SignedIDkgDealing, sign_basic_signed_idkg_dealing); impl_basic_signer!(IDkgDealing, sign_basic_idkg_dealing); impl_basic_signer!(IDkgComplaintContent, sign_basic_idkg_complaint); impl_basic_signer!(IDkgOpeningContent, sign_basic_idkg_opening); -impl_basic_signer!(CanisterHttpResponseMetadata, sign_basic_http); +impl_basic_signer!(CanisterHttpResponseReceipt, sign_basic_http); impl_basic_signer!(QueryResponseHash, sign_basic_query); impl_basic_sig_verifier!( @@ -829,7 +829,7 @@ impl_basic_sig_verifier!( verify_basic_sig_batch_multi_msg_idkg_opening ); impl_basic_sig_verifier!( - CanisterHttpResponseMetadata, + CanisterHttpResponseReceipt, verify_basic_sig_http, combine_basic_sig_http, verify_basic_sig_batch_http, diff --git a/rs/interfaces/src/canister_http.rs b/rs/interfaces/src/canister_http.rs index 7a836ee735fa..da03d4090fd3 100644 --- a/rs/interfaces/src/canister_http.rs +++ b/rs/interfaces/src/canister_http.rs @@ -12,6 +12,7 @@ use ic_types::{ crypto::{CryptoError, CryptoHashOf}, messages::CallbackId, }; +use ic_types_cycles::Cycles; #[derive(Debug)] pub enum InvalidCanisterHttpPayloadReason { @@ -56,6 +57,12 @@ pub enum InvalidCanisterHttpPayloadReason { }, /// There was an error with a signature calculation SignatureError(Box), + /// A payment receipt claims a refund larger than the per-replica allowance + /// derived from the request's payment. + RefundExceedsAllowance { + refund: Cycles, + per_replica_allowance: Cycles, + }, /// Some of the signatures in the canister http proof were not members of /// the canister http committee. SignersNotMembers { diff --git a/rs/interfaces/src/crypto.rs b/rs/interfaces/src/crypto.rs index 3510043c8687..30a7dddbe427 100644 --- a/rs/interfaces/src/crypto.rs +++ b/rs/interfaces/src/crypto.rs @@ -1,7 +1,7 @@ //! The crypto public interface. mod keygen; -use ic_types::canister_http::CanisterHttpResponseMetadata; +use ic_types::canister_http::CanisterHttpResponseReceipt; pub use keygen::*; mod errors; @@ -80,8 +80,8 @@ pub trait Crypto: + ThresholdSchnorrSigVerifier + VetKdProtocol // CanisterHttpResponse - + BasicSigner - + BasicSigVerifier + + BasicSigner + + BasicSigVerifier // Signed Queries + BasicSigner // RequestId/WebAuthn @@ -141,8 +141,8 @@ impl Crypto for T where + BasicSigVerifier + BasicSigner + BasicSigVerifier - + BasicSigner - + BasicSigVerifier + + BasicSigner + + BasicSigVerifier + BasicSigner + IDkgProtocol + ThresholdEcdsaSigner diff --git a/rs/protobuf/def/types/v1/canister_http.proto b/rs/protobuf/def/types/v1/canister_http.proto index c2a21156a99b..f2587b029219 100644 --- a/rs/protobuf/def/types/v1/canister_http.proto +++ b/rs/protobuf/def/types/v1/canister_http.proto @@ -56,6 +56,7 @@ message CanisterHttpReject { message CanisterHttpResponseSignature { bytes signer = 1; bytes signature = 2; + CanisterHttpPaymentReceipt payment_receipt = 3; } message CanisterHttpResponseWithConsensus { diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index 552e975bb2e1..64f8f32a02b4 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -590,6 +590,8 @@ pub struct CanisterHttpResponseSignature { pub signer: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", tag = "2")] pub signature: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "3")] + pub payment_receipt: ::core::option::Option, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct CanisterHttpResponseWithConsensus { diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 050953d5c6b9..6c075cd9bb9a 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -149,8 +149,9 @@ use ic_types::{ ValidationContext, XNetPayload, }, canister_http::{ - CanisterHttpRequestContext, CanisterHttpRequestId, CanisterHttpResponse, - CanisterHttpResponseContent, CanisterHttpResponseMetadata, + CanisterHttpPaymentReceipt, CanisterHttpRequestContext, CanisterHttpRequestId, + CanisterHttpResponse, CanisterHttpResponseContent, CanisterHttpResponseMetadata, + CanisterHttpResponseReceipt, }, consensus::{ block_maker::SubnetRecords, @@ -1885,7 +1886,7 @@ impl StateMachine { .unwrap() .get_validated_shares() .filter_map(|share| { - if inducted.contains(&share.content.id) { + if inducted.contains(&share.content.id()) { Some(CanisterHttpChangeAction::RemoveValidated(share.clone())) } else { None @@ -2718,19 +2719,22 @@ impl StateMachine { canister_id, content: content.clone(), }; - let response_metadata = CanisterHttpResponseMetadata { - id: CallbackId::from(request_id), - registry_version, - content_hash: ic_types::crypto::crypto_hash(&response), - content_size: content.count_bytes() as u32, - is_reject: content.is_reject(), - replica_version: ReplicaVersion::default(), + let receipt_share = CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CallbackId::from(request_id), + registry_version, + content_hash: ic_types::crypto::crypto_hash(&response), + content_size: content.count_bytes() as u32, + is_reject: content.is_reject(), + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt::default(), }; let signature = CryptoReturningOk::default() - .sign(&response_metadata, node.node_id, registry_version) + .sign(&receipt_share, node.node_id, registry_version) .unwrap(); let share = Signed { - content: response_metadata, + content: receipt_share, signature, }; self.canister_http_pool.write().unwrap().apply(vec![ @@ -5133,7 +5137,7 @@ impl StateMachine { .read() .unwrap() .get_validated_shares() - .map(|share| share.content.id) + .map(|share| share.content.id()) .collect(); let state = self.state_manager.get_latest_state().take(); state diff --git a/rs/types/types/src/batch/canister_http.rs b/rs/types/types/src/batch/canister_http.rs index f22811d17448..4ab531e4f109 100644 --- a/rs/types/types/src/batch/canister_http.rs +++ b/rs/types/types/src/batch/canister_http.rs @@ -3,12 +3,13 @@ use crate::{ canister_http::{ CanisterHttpPaymentReceipt, CanisterHttpReject, CanisterHttpRequestId, CanisterHttpResponse, CanisterHttpResponseArtifact, CanisterHttpResponseContent, - CanisterHttpResponseDivergence, CanisterHttpResponseMetadata, CanisterHttpResponseShare, + CanisterHttpResponseDivergence, CanisterHttpResponseMetadata, CanisterHttpResponseProof, + CanisterHttpResponseReceipt, CanisterHttpResponseShare, CanisterHttpResponseSignature, CanisterHttpResponseWithConsensus, }, crypto::{BasicSig, BasicSigOf, CryptoHash, CryptoHashOf, Signed}, messages::CallbackId, - signature::{BasicSignature, BasicSignatureBatch}, + signature::BasicSignature, }; use ic_base_types::{NodeId, PrincipalId, RegistryVersion}; use ic_error_types::RejectCode; @@ -201,6 +202,10 @@ impl TryFrom for CanisterHttpPaymentReceipt { impl From for pb::CanisterHttpResponseWithConsensus { fn from(payload: CanisterHttpResponseWithConsensus) -> Self { + let CanisterHttpResponseProof { + metadata, + signatures, + } = payload.proof; pb::CanisterHttpResponseWithConsensus { response: Some(pb::CanisterHttpResponse { id: payload.content.id.get(), @@ -209,21 +214,19 @@ impl From for pb::CanisterHttpResponseWithCon )), canister_id: Some(pb::CanisterId::from(payload.content.canister_id)), }), - hash: payload.proof.content.content_hash.get().0, - registry_version: payload.proof.content.registry_version.get(), - replica_version: payload.proof.content.replica_version.into(), - signatures: payload - .proof - .signature - .signatures_map + hash: metadata.content_hash.get().0, + registry_version: metadata.registry_version.get(), + replica_version: metadata.replica_version.into(), + signatures: signatures .into_iter() - .map(|(signer, signature)| pb::CanisterHttpResponseSignature { + .map(|(signer, sig)| pb::CanisterHttpResponseSignature { signer: signer.get().into_vec(), - signature: signature.get().0, + signature: sig.signature.get().0, + payment_receipt: Some(sig.payment_receipt.into()), }) .collect(), - content_size: payload.proof.content.content_size, - is_reject: payload.proof.content.is_reject, + content_size: metadata.content_size, + is_reject: metadata.is_reject, } } } @@ -249,6 +252,22 @@ impl TryFrom for CanisterHttpResponseWith "CanisterHttpResponseWithConsensus::canister_id", )?; + let mut signatures = BTreeMap::new(); + for signature in payload.signatures { + let signer = NodeId::from(PrincipalId::try_from(signature.signer)?); + let payment_receipt = try_from_option_field( + signature.payment_receipt, + "CanisterHttpResponseSignature::payment_receipt", + )?; + signatures.insert( + signer, + CanisterHttpResponseSignature { + payment_receipt, + signature: BasicSigOf::new(BasicSig(signature.signature)), + }, + ); + } + Ok(CanisterHttpResponseWithConsensus { content: CanisterHttpResponse { id, @@ -258,8 +277,8 @@ impl TryFrom for CanisterHttpResponseWith "CanisterHttpResponseWithConsensus::content", )?, }, - proof: Signed { - content: CanisterHttpResponseMetadata { + proof: CanisterHttpResponseProof { + metadata: CanisterHttpResponseMetadata { id, content_hash: CryptoHashOf::::new(CryptoHash( payload.hash, @@ -270,18 +289,7 @@ impl TryFrom for CanisterHttpResponseWith replica_version: ReplicaVersion::try_from(payload.replica_version) .map_err(|err| ProxyDecodeError::ReplicaVersionParseError(Box::new(err)))?, }, - signature: BasicSignatureBatch { - signatures_map: payload - .signatures - .into_iter() - .map(|signature| { - Ok(( - NodeId::from(PrincipalId::try_from(signature.signer)?), - BasicSigOf::new(BasicSig(signature.signature)), - )) - }) - .collect::>, ProxyDecodeError>>()?, - }, + signatures, }, }) } @@ -354,18 +362,23 @@ impl TryFrom for CanisterHttpResponseContent { impl From for pb::CanisterHttpShare { fn from(share: CanisterHttpResponseShare) -> Self { + let CanisterHttpResponseReceipt { + metadata, + payment_receipt, + } = share.content; pb::CanisterHttpShare { metadata: Some(pb::CanisterHttpResponseMetadata { - id: share.content.id.get(), - content_hash: share.content.content_hash.clone().get().0, - registry_version: share.content.registry_version.get(), - replica_version: share.content.replica_version.into(), - content_size: share.content.content_size, - is_reject: share.content.is_reject, + id: metadata.id.get(), + content_hash: metadata.content_hash.get().0, + registry_version: metadata.registry_version.get(), + replica_version: metadata.replica_version.into(), + content_size: metadata.content_size, + is_reject: metadata.is_reject, }), signature: Some(pb::CanisterHttpResponseSignature { signer: share.signature.signer.get().into_vec(), - signature: share.signature.signature.clone().get().0, + signature: share.signature.signature.get().0, + payment_receipt: Some(payment_receipt.into()), }), } } @@ -378,21 +391,28 @@ impl TryFrom for CanisterHttpResponseShare { .metadata .ok_or(ProxyDecodeError::MissingField("share.metadata"))?; let id = CanisterHttpRequestId::new(metadata.id); - let content_hash = CryptoHashOf::new(CryptoHash(metadata.content_hash.clone())); + let content_hash = CryptoHashOf::new(CryptoHash(metadata.content_hash)); let registry_version = RegistryVersion::new(metadata.registry_version); let replica_version = ReplicaVersion::try_from(metadata.replica_version) .map_err(|err| ProxyDecodeError::ReplicaVersionParseError(Box::new(err)))?; let signature = share .signature .ok_or(ProxyDecodeError::MissingField("share.signature"))?; + let payment_receipt = try_from_option_field( + signature.payment_receipt, + "CanisterHttpResponseSignature::payment_receipt", + )?; Ok(Signed { - content: CanisterHttpResponseMetadata { - id, - content_hash, - content_size: metadata.content_size, - is_reject: metadata.is_reject, - registry_version, - replica_version, + content: CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id, + content_hash, + content_size: metadata.content_size, + is_reject: metadata.is_reject, + registry_version, + replica_version, + }, + payment_receipt, }, signature: BasicSignature { signer: NodeId::from(PrincipalId::try_from(signature.signer)?), @@ -588,6 +608,7 @@ mod tests { use super::*; use crate::exhaustive::ExhaustiveSet; use ic_crypto_test_utils_reproducible_rng::ReproducibleRng; + use ic_types_cycles::Cycles; /// Tests that a roundtrip of protobuf conversions for `CanisterHttpResponse` /// works correctly. @@ -611,19 +632,25 @@ mod tests { /// works correctly, both with and without a full response. #[test] fn canister_http_response_artifact_conversion() { + let signer = NodeId::from(PrincipalId::new_node_test_id(2)); let share = Signed { - content: CanisterHttpResponseMetadata { - id: CanisterHttpRequestId::new(2), - content_hash: CryptoHashOf::::new(CryptoHash(vec![ - 4, 5, 6, 7, - ])), - content_size: 42, - is_reject: false, - registry_version: RegistryVersion::new(2), - replica_version: ReplicaVersion::default(), + content: CanisterHttpResponseReceipt { + metadata: CanisterHttpResponseMetadata { + id: CanisterHttpRequestId::new(2), + content_hash: CryptoHashOf::::new(CryptoHash(vec![ + 4, 5, 6, 7, + ])), + content_size: 42, + is_reject: false, + registry_version: RegistryVersion::new(2), + replica_version: ReplicaVersion::default(), + }, + payment_receipt: CanisterHttpPaymentReceipt { + refund: Cycles::new(42), + }, }, signature: BasicSignature { - signer: NodeId::from(PrincipalId::new_node_test_id(2)), + signer, signature: BasicSigOf::new(BasicSig(vec![4, 5, 6, 7])), }, }; @@ -677,7 +704,7 @@ mod tests { // store the id separately in the metadata — it reuses the response's // value on deserialization. Normalize here so the roundtrip // comparison holds. - response.proof.content.id = response.content.id; + response.proof.metadata.id = response.content.id; let pb = pb::CanisterHttpResponseWithConsensus::from(response.clone()); let roundtripped = CanisterHttpResponseWithConsensus::try_from(pb).unwrap(); diff --git a/rs/types/types/src/canister_http.rs b/rs/types/types/src/canister_http.rs index 7a44c214e35f..fe452bc7b2d6 100644 --- a/rs/types/types/src/canister_http.rs +++ b/rs/types/types/src/canister_http.rs @@ -44,7 +44,7 @@ use crate::{ CanisterId, CountBytes, RegistryVersion, ReplicaVersion, Time, artifact::{CanisterHttpResponseId, IdentifiableArtifact, PbArtifact}, - crypto::{CryptoHashOf, Signed}, + crypto::{BasicSigOf, CryptoHashOf}, messages::{CallbackId, RejectContext, Request}, node_id_into_protobuf, node_id_try_from_protobuf, signature::*, @@ -67,7 +67,7 @@ use rand::RngCore; use rand::seq::IteratorRandom; use serde::{Deserialize, Serialize}; use std::{ - collections::BTreeSet, + collections::{BTreeMap, BTreeSet}, convert::{TryFrom, TryInto}, mem::size_of, time::Duration, @@ -1081,15 +1081,117 @@ impl CountBytes for CanisterHttpResponseMetadata { } } -impl crate::crypto::SignedBytesWithoutDomainSeparator for CanisterHttpResponseMetadata { +/// The content a single replica signs over: the shared +/// [`CanisterHttpResponseMetadata`] together with that replica's own +/// [`CanisterHttpPaymentReceipt`]. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(ExhaustiveSet))] +pub struct CanisterHttpResponseReceipt { + pub metadata: CanisterHttpResponseMetadata, + pub payment_receipt: CanisterHttpPaymentReceipt, +} + +impl CountBytes for CanisterHttpResponseReceipt { + fn count_bytes(&self) -> usize { + let Self { + metadata, + payment_receipt, + } = self; + metadata.count_bytes() + payment_receipt.count_bytes() + } +} + +impl CanisterHttpResponseReceipt { + pub fn id(&self) -> CallbackId { + self.metadata.id + } + + pub fn content_hash(&self) -> &CryptoHashOf { + &self.metadata.content_hash + } + + pub fn content_size(&self) -> u32 { + self.metadata.content_size + } + + pub fn is_reject(&self) -> bool { + self.metadata.is_reject + } + + pub fn registry_version(&self) -> RegistryVersion { + self.metadata.registry_version + } + + pub fn replica_version(&self) -> &ReplicaVersion { + &self.metadata.replica_version + } + + pub fn refund(&self) -> Cycles { + self.payment_receipt.refund + } +} + +impl crate::crypto::SignedBytesWithoutDomainSeparator for CanisterHttpResponseReceipt { fn write_signed_bytes_without_domain_separator(&self, bytes: &mut Vec) { serde_cbor::to_writer(bytes, &self).unwrap(); } } -/// A signature share of of [`CanisterHttpResponseMetadata`]. -pub type CanisterHttpResponseShare = - Signed>; +/// A single signer's contribution to an aggregated proof: the +/// [`CanisterHttpPaymentReceipt`] that signer signed over, together with +/// their basic signature on the corresponding +/// [`CanisterHttpResponseReceipt`]. +#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(ExhaustiveSet))] +pub struct CanisterHttpResponseSignature { + pub payment_receipt: CanisterHttpPaymentReceipt, + pub signature: BasicSigOf, +} + +impl CountBytes for CanisterHttpResponseSignature { + fn count_bytes(&self) -> usize { + let Self { + payment_receipt, + signature, + } = self; + payment_receipt.count_bytes() + signature.get_ref().count_bytes() + } +} + +/// An aggregated proof for a canister HTTP response with consensus. +/// +/// Holds the shared [`CanisterHttpResponseMetadata`] together with, for +/// each contributing signer, the [`CanisterHttpPaymentReceipt`] they +/// signed over and their basic signature (see [`CanisterHttpResponseSignature`]). +#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(ExhaustiveSet))] +pub struct CanisterHttpResponseProof { + pub metadata: CanisterHttpResponseMetadata, + pub signatures: BTreeMap, +} + +impl CountBytes for CanisterHttpResponseProof { + fn count_bytes(&self) -> usize { + let Self { + metadata, + signatures, + } = self; + metadata.count_bytes() + + signatures + .values() + .map(|s| std::mem::size_of::() + s.count_bytes()) + .sum::() + } +} + +impl CanisterHttpResponseProof { + pub fn registry_version(&self) -> RegistryVersion { + self.metadata.registry_version + } +} + +/// A signature share of [`CanisterHttpResponseReceipt`]. +pub type CanisterHttpResponseShare = BasicSigned; /// Contains a share and optionally the full response. /// @@ -1116,10 +1218,6 @@ impl PbArtifact for CanisterHttpResponseArtifact { type PbMessageError = ProxyDecodeError; } -/// A signature of of [`CanisterHttpResponseMetadata`]. -pub type CanisterHttpResponseProof = - Signed>; - #[cfg(test)] mod tests { use crate::{messages::NO_DEADLINE, time::UNIX_EPOCH}; diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index 27194a242a79..53090917a59b 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -70,8 +70,8 @@ mod crypto_hash_stability { use crate::CryptoHashOfState; use crate::batch::{BatchPayload, ValidationContext}; use crate::canister_http::{ - CanisterHttpRequestId, CanisterHttpResponse, CanisterHttpResponseContent, - CanisterHttpResponseMetadata, + CanisterHttpPaymentReceipt, CanisterHttpRequestId, CanisterHttpResponse, + CanisterHttpResponseContent, CanisterHttpResponseMetadata, CanisterHttpResponseReceipt, }; use crate::consensus::{ Block, BlockPayload, BlockProposal, CatchUpContent, CatchUpContentProtobufBytes, @@ -126,6 +126,7 @@ mod crypto_hash_stability { use ic_crypto_test_utils_ni_dkg::ni_dkg_csp_dealing; use ic_crypto_tree_hash::{Digest, Witness}; use ic_protobuf::types::v1 as pb; + use ic_types_cycles::Cycles; use std::collections::BTreeMap; use std::sync::Arc; @@ -1094,8 +1095,14 @@ mod crypto_hash_stability { registry_version: RegistryVersion::from(1), replica_version: ReplicaVersion::default(), }; + let receipt_share = CanisterHttpResponseReceipt { + metadata, + payment_receipt: CanisterHttpPaymentReceipt { + refund: Cycles::new(42), + }, + }; let data = Signed { - content: metadata, + content: receipt_share, signature: BasicSignature { signature: BasicSigOf::new(BasicSig(vec![0x42; 64])), signer: NodeId::from(PrincipalId::new_node_test_id(42)), @@ -1104,7 +1111,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "7bff0af6053ad0f648acffecf0434e299e9ce1d04b6752934935a13e390de986", + "a9372188df0e1057515013fa0208e8a6bf6aec7a5f5271c02585454c2bd32a2a", "Hash of CanisterHttpResponseShare changed" ); } diff --git a/rs/types/types/src/crypto/sign.rs b/rs/types/types/src/crypto/sign.rs index d4d886e29d69..115142145aec 100644 --- a/rs/types/types/src/crypto/sign.rs +++ b/rs/types/types/src/crypto/sign.rs @@ -1,7 +1,7 @@ //! Defines signature types. use super::hash::domain_separator::DomainSeparator; -use crate::canister_http::CanisterHttpResponseMetadata; +use crate::canister_http::CanisterHttpResponseReceipt; use crate::consensus::{ BlockMetadata, CatchUpContent, CatchUpContentProtobufBytes, FinalizationContent, NotarizationContent, RandomBeaconContent, RandomTapeContent, @@ -70,7 +70,7 @@ mod private { impl SignatureDomainSeal for IDkgOpeningContent {} impl SignatureDomainSeal for WebAuthnEnvelope {} impl SignatureDomainSeal for Delegation {} - impl SignatureDomainSeal for CanisterHttpResponseMetadata {} + impl SignatureDomainSeal for CanisterHttpResponseReceipt {} impl SignatureDomainSeal for MessageId {} impl<'a> SignatureDomainSeal for SenderInfoContent<'a> {} impl SignatureDomainSeal for CertificationContent {} @@ -83,7 +83,7 @@ mod private { impl SignatureDomainSeal for VetKdEncryptedKeyShareContent {} } -impl SignatureDomain for CanisterHttpResponseMetadata { +impl SignatureDomain for CanisterHttpResponseReceipt { fn domain(&self) -> Vec { domain_with_prepended_length( DomainSeparator::CryptoHashOfCanisterHttpResponseMetadata.as_str(), diff --git a/rs/types/types/src/exhaustive.rs b/rs/types/types/src/exhaustive.rs index 59879ba83bdd..95d399fb41fe 100644 --- a/rs/types/types/src/exhaustive.rs +++ b/rs/types/types/src/exhaustive.rs @@ -2,6 +2,7 @@ use crate::artifact::IngressMessageId; use crate::batch::ChainKeyAgreement; +use crate::canister_http::CanisterHttpResponseSignature; use crate::consensus::dkg::RemoteDkgAttempts; use crate::consensus::hashed::Hashed; use crate::consensus::idkg::IDkgMasterPublicKeyId; @@ -601,7 +602,7 @@ impl ExhaustiveSet for Signed ExhaustiveSet for Signed> { +impl ExhaustiveSet for Signed> { fn exhaustive_set(rng: &mut R) -> Vec { let signatures_map: BTreeMap<_, _> = NodeId::exhaustive_set(rng) .into_iter() @@ -1030,6 +1031,7 @@ impl HasId for CertifiedStreamSlice {} impl HasId for RemoteDkgAttempts {} impl HasId for PreSignatureInCreation {} impl HasId for PreSignatureRef {} +impl HasId for CanisterHttpResponseSignature {} #[cfg(test)] mod tests { From 557ddcf7a96a7c8e6759454a2238aa7e14917cfd Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:20:56 +0200 Subject: [PATCH 12/75] feat: enable canister log memory store feature (#10160) This PR enables the canister log memory store feature on all subnets. --- rs/config/src/execution_environment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/config/src/execution_environment.rs b/rs/config/src/execution_environment.rs index 87e62fddb709..43686e93fe63 100644 --- a/rs/config/src/execution_environment.rs +++ b/rs/config/src/execution_environment.rs @@ -14,7 +14,7 @@ const TIB: u64 = 1024 * GIB; const REPLICATED_INTER_CANISTER_LOG_FETCH_FEATURE: FlagStatus = FlagStatus::Disabled; // TODO(DSM-105): remove after the feature is enabled by default. -pub const LOG_MEMORY_STORE_FEATURE_ENABLED: bool = false; +pub const LOG_MEMORY_STORE_FEATURE_ENABLED: bool = true; pub const LOG_MEMORY_STORE_FEATURE: FlagStatus = if LOG_MEMORY_STORE_FEATURE_ENABLED { FlagStatus::Enabled } else { From 04f4e8582eaf02ad7e12f6461fde303b46851eac Mon Sep 17 00:00:00 2001 From: michael-weigelt <122277901+michael-weigelt@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:25:26 +0200 Subject: [PATCH 13/75] chore(Core): Limit wasm locals (#10430) --- rs/embedders/src/wasm_utils/validation.rs | 17 +++++++++++++++ rs/embedders/tests/validation.rs | 26 ++++++++++++++++++++++- rs/types/wasm_types/src/errors.rs | 19 +++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/rs/embedders/src/wasm_utils/validation.rs b/rs/embedders/src/wasm_utils/validation.rs index ca21ec9487b2..b369e49d836e 100644 --- a/rs/embedders/src/wasm_utils/validation.rs +++ b/rs/embedders/src/wasm_utils/validation.rs @@ -67,6 +67,7 @@ const WASM_FUNCTION_COMPLEXITY_LIMIT: Complexity = Complexity(1_000_000); pub const WASM_FUNCTION_SIZE_LIMIT: usize = 1_000_000; pub const MAX_CODE_SECTION_SIZE_IN_BYTES: u32 = 12 * 1024 * 1024; pub const MAX_WASM_FUNCTION_NAME_LENGTH: usize = 1024 * 1024; +pub const MAX_WASM_FUNCTION_NUM_LOCALS: usize = 2000; // Represents the expected function signature for any System APIs the Internet // Computer provides or any special exported user functions. @@ -1325,6 +1326,22 @@ fn validate_function_section( name: truncated_name, }); } + // Check number of locals + let num_locals = module + .functions + .get(id) + .kind() + .unwrap_local() /* we are looping over locals only */ + .unwrap() /* we retrieved the id from the same module */ + .body + .num_locals; + if num_locals > MAX_WASM_FUNCTION_NUM_LOCALS as u32 { + return Err(WasmValidationError::TooManyLocals { + index: *id as usize, + defined: num_locals as usize, + allowed: MAX_WASM_FUNCTION_NUM_LOCALS, + }); + } } Ok(()) diff --git a/rs/embedders/tests/validation.rs b/rs/embedders/tests/validation.rs index ac2743971983..a6cb4a498e49 100644 --- a/rs/embedders/tests/validation.rs +++ b/rs/embedders/tests/validation.rs @@ -7,7 +7,8 @@ use ic_embedders::{ wasm_utils::{ Complexity, WasmImportsDetails, WasmValidationDetails, validate_and_instrument_for_testing, validation::{ - MAX_WASM_FUNCTION_NAME_LENGTH, RESERVED_SYMBOLS, extract_custom_section_name, + MAX_WASM_FUNCTION_NAME_LENGTH, MAX_WASM_FUNCTION_NUM_LOCALS, RESERVED_SYMBOLS, + extract_custom_section_name, }, }, }; @@ -1478,3 +1479,26 @@ fn wasm_with_long_func_name_is_invalid() { }) ); } + +#[test] +fn wasm_with_many_locals_is_invalid() { + let wat = format!( + r#" + (module + (func $f (export "canister_update f") + (local {}) + ) + )"#, + "i32 ".repeat(MAX_WASM_FUNCTION_NUM_LOCALS + 1) + ); + + let wasm = wat2wasm(&wat).unwrap(); + assert_eq!( + validate_wasm_binary(&wasm, &EmbeddersConfig::default()), + Err(WasmValidationError::TooManyLocals { + index: 0, + defined: MAX_WASM_FUNCTION_NUM_LOCALS + 1, + allowed: MAX_WASM_FUNCTION_NUM_LOCALS, + }) + ); +} diff --git a/rs/types/wasm_types/src/errors.rs b/rs/types/wasm_types/src/errors.rs index 6a655344ba0a..830025069623 100644 --- a/rs/types/wasm_types/src/errors.rs +++ b/rs/types/wasm_types/src/errors.rs @@ -140,6 +140,12 @@ pub enum WasmValidationError { allowed: usize, name: String, }, + /// A function contains too many locals. + TooManyLocals { + index: usize, + defined: usize, + allowed: usize, + }, /// The code section is too large. CodeSectionTooLarge { size: u32, @@ -263,6 +269,15 @@ impl std::fmt::Display for WasmValidationError { "Wasm module contains a function at index {index} \ with name '{name}' of size {size} bytes that exceeds the maximum allowed size of {allowed} bytes.", ), + Self::TooManyLocals { + index, + defined, + allowed, + } => write!( + f, + "Wasm module contains a function at index {index} \ + with {defined} locals that exceeds the maximum allowed number of locals {allowed}" + ), Self::CodeSectionTooLarge { size, allowed } => write!( f, "Wasm module code section size of {size} \ @@ -340,6 +355,10 @@ impl AsErrorHelp for WasmValidationError { suggestion: "Try using shorter function names.".to_string(), doc_link: doc_ref("wasm-module-function-name-too-large"), }, + WasmValidationError::TooManyLocals { .. } => ErrorHelp::UserError { + suggestion: "Try different optimizer settings.".to_string(), + doc_link: doc_ref("wasm-module-too-many-locals"), + }, WasmValidationError::CodeSectionTooLarge { .. } => ErrorHelp::UserError { suggestion: "Try shrinking the module code section using tools like \ `ic-wasm` or splitting the logic across multiple canisters." From 363a6414fecf0567035180091446e0de38d64536 Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Thu, 11 Jun 2026 13:26:39 +0200 Subject: [PATCH 14/75] fix(NODE-1958): Make cloud GuestOS great again (#10429) It seems that Ubuntu 26 upgrade changed the way the network is initialized by `systemd` which breaks our intermediate DHCP step. It's hard to debug especially in combination with SELinux policies. * Use `dhcpcd` instead of `systemd-networkd` to obtain a temporary DHCP address. This avoids problems with new Ubuntu 26 init order (`systemd-networkd` is started now by `systemd` early it seems and if we launch a 2nd instance - it falls apart). * Remove `console=tty0` from kernel boot args. It was added to support logging on serial-port-less clouds, but the problem is that when both `ttyX` and `ttySX` are present - the kernel makes the normal (graphical / ttyX) console the main one (regardless of their order) and serial console doesn't get any recovery shell or proper logging. We can try later to improve that somehow to get the best of the both worlds, though it's not easy. Also see https://github.com/systemd/systemd/issues/9899 about that. * Alter the `icos_logging` so that it logs to both `stderr` and `journald` by default to ease debugging * Remove timeout for `init-config` service since it anyway is critical and it makes not much sense to kill it if it hangs around longer * Add timeouts to cloud-related HTTP calls, add more verbose logging --- Cargo.lock | 1 + ic-os/bootloader/guestos_boot_args.template | 8 +- .../init/init-config/init-config.service | 1 - rs/ic_os/config/tool/BUILD.bazel | 4 + rs/ic_os/config/tool/Cargo.toml | 1 + rs/ic_os/config/tool/src/guestos/cloud.rs | 39 ++++++--- rs/ic_os/logging/src/lib.rs | 18 ++-- .../guestos_tool/src/cloud_provision.rs | 84 +++++++++---------- rs/ic_os/os_tools/guestos_tool/src/main.rs | 25 +++--- 9 files changed, 105 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b85fc3a9c9c..324d36b00ebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2664,6 +2664,7 @@ dependencies = [ "sev_guest", "strum 0.26.3", "tempfile", + "tracing", "url", "utils", ] diff --git a/ic-os/bootloader/guestos_boot_args.template b/ic-os/bootloader/guestos_boot_args.template index bd58c8ecd07d..60b9366f1f0e 100644 --- a/ic-os/bootloader/guestos_boot_args.template +++ b/ic-os/bootloader/guestos_boot_args.template @@ -5,7 +5,7 @@ # the system will use SELinux and keep track of operations that would # be prohibited, but will only log but not actually deny them. This is # useful for debug and policy development. -BOOT_ARGS_A="root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A security=selinux selinux=1 enforcing=1 root_hash=ROOT_HASH" -BOOT_ARGS_B="root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B security=selinux selinux=1 enforcing=1 root_hash=ROOT_HASH" -BOOT_ARGS_TEE_A="root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ROOT_HASH" -BOOT_ARGS_TEE_B="root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ROOT_HASH" +BOOT_ARGS_A="root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A security=selinux selinux=1 enforcing=1 root_hash=ROOT_HASH" +BOOT_ARGS_B="root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B security=selinux selinux=1 enforcing=1 root_hash=ROOT_HASH" +BOOT_ARGS_TEE_A="root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ROOT_HASH" +BOOT_ARGS_TEE_B="root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ROOT_HASH" diff --git a/ic-os/components/guestos/init/init-config/init-config.service b/ic-os/components/guestos/init/init-config/init-config.service index eaebd6abe7d1..e63294b967f9 100644 --- a/ic-os/components/guestos/init/init-config/init-config.service +++ b/ic-os/components/guestos/init/init-config/init-config.service @@ -8,7 +8,6 @@ RequiresMountsFor=/run Type=oneshot RemainAfterExit=true ExecStart=/opt/ic/bin/init-config.sh -TimeoutSec=60 Restart=on-failure RestartSec=10 diff --git a/rs/ic_os/config/tool/BUILD.bazel b/rs/ic_os/config/tool/BUILD.bazel index 3b115dcad4b2..d83f920ebbdc 100644 --- a/rs/ic_os/config/tool/BUILD.bazel +++ b/rs/ic_os/config/tool/BUILD.bazel @@ -51,6 +51,7 @@ rust_library( "@crate_index//:serde_with", "@crate_index//:strum", "@crate_index//:tempfile", + "@crate_index//:tracing", "@crate_index//:url", ], ) @@ -91,6 +92,7 @@ rust_library( "@crate_index//:serde_with", "@crate_index//:strum", "@crate_index//:tempfile", + "@crate_index//:tracing", "@crate_index//:url", ], ) @@ -154,6 +156,7 @@ rust_binary( "@crate_index//:serde_with", "@crate_index//:strum", "@crate_index//:tempfile", + "@crate_index//:tracing", "@crate_index//:url", ], ) @@ -194,6 +197,7 @@ rust_binary( "@crate_index//:serde_with", "@crate_index//:strum", "@crate_index//:tempfile", + "@crate_index//:tracing", "@crate_index//:url", ], ) diff --git a/rs/ic_os/config/tool/Cargo.toml b/rs/ic_os/config/tool/Cargo.toml index c457b1ac59f7..2b4e50a74311 100644 --- a/rs/ic_os/config/tool/Cargo.toml +++ b/rs/ic_os/config/tool/Cargo.toml @@ -28,6 +28,7 @@ getifs = { workspace = true } sev_guest = { path = "../../sev/guest" } strum = { workspace = true } securefmt = { workspace = true } +tracing = { workspace = true } [dev-dependencies] once_cell = "1.8" diff --git a/rs/ic_os/config/tool/src/guestos/cloud.rs b/rs/ic_os/config/tool/src/guestos/cloud.rs index 1b9f9ac3b201..27c55aa23c34 100644 --- a/rs/ic_os/config/tool/src/guestos/cloud.rs +++ b/rs/ic_os/config/tool/src/guestos/cloud.rs @@ -4,18 +4,20 @@ use std::{ time::Duration, }; +use ::reqwest::Method; use anyhow::{Context, Error, Result, anyhow, bail}; use config_types::GuestOSConfig; - use reqwest::header::{HeaderMap, HeaderValue}; - -use ::reqwest::Method; use serde_json::Value; use strum::{Display, EnumString}; +use tracing::info; /// URL of the metadata server const METADATA_URL: &str = "http://169.254.169.254"; +/// Timeout for HTTP calls +const TIMEOUT: Duration = Duration::from_secs(30); + /// Type of the cloud that we provision from #[derive(Debug, Clone, PartialEq, Eq, Display, EnumString)] pub enum CloudType { @@ -27,10 +29,14 @@ pub enum CloudType { impl CloudType { /// Discovers the cloud type by making a request to the metadata server pub fn discover() -> Result { - let mut retries = 30; + let mut retries = 120; let resp = loop { - match reqwest::blocking::get(METADATA_URL) { + let mut req = + reqwest::blocking::Request::new(Method::GET, METADATA_URL.parse().unwrap()); + *req.timeout_mut() = Some(TIMEOUT); + + match reqwest::blocking::Client::new().execute(req) { Ok(v) => break v, Err(e) => { retries -= 1; @@ -38,7 +44,7 @@ impl CloudType { return Err(anyhow!("unable to discover cloud type: retries exhausted")); } - println!("Unable to contact metadata server (retries left {retries}): {e:#}"); + info!("Unable to contact metadata server (retries left {retries}): {e:#}"); std::thread::sleep(Duration::from_secs(1)); } } @@ -50,12 +56,20 @@ impl CloudType { /// Tries to fetch the GuestOS config from the cloud's metadata service pub fn obtain_config(&self) -> Result { let json = match self { - Self::Aws => reqwest::blocking::get(format!("{METADATA_URL}/latest/user-data")) - .context("unable to execute request")? - .bytes() - .context("unable to fetch config JSON")? - .to_vec(), + Self::Aws => { + let mut req = reqwest::blocking::Request::new( + Method::GET, + format!("{METADATA_URL}/latest/user-data").parse().unwrap(), + ); + *req.timeout_mut() = Some(TIMEOUT); + reqwest::blocking::Client::new() + .execute(req) + .context("unable to execute request")? + .bytes() + .context("unable to fetch config JSON")? + .to_vec() + } Self::Gcp => { let mut req = reqwest::blocking::Request::new( Method::GET, @@ -63,6 +77,7 @@ impl CloudType { .parse() .unwrap(), ); + *req.timeout_mut() = Some(TIMEOUT); req.headers_mut() .insert("Metadata-Flavor", "Google".try_into().unwrap()); @@ -76,6 +91,7 @@ impl CloudType { Self::Azure => { let mut req = reqwest::blocking::Request::new(Method::GET, format!("{METADATA_URL}/metadata/instance/compute/userData?api-version=2025-04-07&format=text").parse().unwrap()); + *req.timeout_mut() = Some(TIMEOUT); req.headers_mut() .insert("Metadata", "true".try_into().unwrap()); @@ -108,6 +124,7 @@ impl CloudType { .parse() .unwrap(), ); + *req.timeout_mut() = Some(TIMEOUT); req.headers_mut() .insert("Metadata", "true".try_into().unwrap()); diff --git a/rs/ic_os/logging/src/lib.rs b/rs/ic_os/logging/src/lib.rs index 3543784e49f9..9b7fa2bf0f43 100644 --- a/rs/ic_os/logging/src/lib.rs +++ b/rs/ic_os/logging/src/lib.rs @@ -7,7 +7,7 @@ use tracing_subscriber::{ Layer, filter::LevelFilter, fmt::{ - FmtContext, format, + FmtContext, format::{FormatEvent, FormatFields, Writer}, }, layer::SubscriberExt, @@ -65,22 +65,28 @@ fn syslog_identifier() -> String { syslog_identifier_from_arg0(arg0.as_deref()) } -/// Initialize tracing, using journald if available and falling back to stderr. +/// Initialize tracing, using journald+stderr if journald is available and falling back to just stderr. +/// Logs with TRACE level. pub fn init_logging() { init_logging_with_level(Level::TRACE); } -/// Initialize tracing, using journald if available and falling back to stderr. +/// Initialize tracing, using journald+stderr if journald is available and falling back to just stderr. pub fn init_logging_with_level(max_level: Level) { let level_filter = LevelFilter::from_level(max_level); match tracing_journald::layer() { Ok(layer) => tracing_subscriber::registry() .with(layer.with_filter(level_filter)) + .with( + tracing_subscriber::fmt::layer() + .with_writer(std::io::stderr) + .with_filter(level_filter), + ) .init(), Err(_) => tracing_subscriber::fmt() .with_writer(std::io::stderr) - .with_max_level(max_level) + .with_max_level(level_filter) .init(), } } @@ -95,7 +101,7 @@ pub fn init_kmsg_logging() { tracing_subscriber::fmt::layer() .event_format(KmsgFormatter { identifier: identifier.clone(), - inner: format() + inner: tracing_subscriber::fmt::format() .without_time() .with_target(false) .with_level(false) @@ -113,7 +119,7 @@ pub fn init_kmsg_logging() { tracing_subscriber::fmt::layer() .event_format(KmsgFormatter { identifier, - inner: format() + inner: tracing_subscriber::fmt::format() .without_time() .with_target(false) .with_level(false) diff --git a/rs/ic_os/os_tools/guestos_tool/src/cloud_provision.rs b/rs/ic_os/os_tools/guestos_tool/src/cloud_provision.rs index b6e11ff1daed..9889dfa8a53a 100644 --- a/rs/ic_os/os_tools/guestos_tool/src/cloud_provision.rs +++ b/rs/ic_os/os_tools/guestos_tool/src/cloud_provision.rs @@ -1,5 +1,4 @@ use std::{ - path::PathBuf, process::{Child, Command, Stdio}, time::Duration, }; @@ -13,67 +12,56 @@ use tracing::info; /// Assigns IPv4 DHCP address to the interface and unassigns it when dropped #[derive(Debug)] struct DHCPConfig { - config_path: PathBuf, process: Child, } impl DHCPConfig { - /// Installs a temporary systemd-networkd config to obtain a DHCP lease - fn new(interface: String, systemd_network_dir: PathBuf) -> Result { - std::fs::create_dir_all(&systemd_network_dir) - .context("unable to create the systemd-networkd dir")?; - - let intf_config = indoc::formatdoc!( - r#" - [Match] - Name={interface} - Virtualization=!container - - [Network] - DHCP=ipv4 - "#, - ); - - // Write the config - let config_path = systemd_network_dir.join(format!("10-{interface}.network")); - std::fs::write(&config_path, intf_config) - .context("unable to write systemd network config")?; - - // Fire up systemd-networkd - let process = Command::new("/usr/lib/systemd/systemd-networkd") + /// Starts dhcpcd to obtain a DHCP lease + fn new(interface: String) -> Result { + // Fire up DHCP client. + // Avoid reading default config since it might contain unneeded options and they take precedence. + info!("Starting dhcpcd"); + let process = Command::new("/usr/sbin/dhcpcd") + .args([ + "--nobackground", + "--ipv4only", + "--noipv4ll", + "--config", + "/dev/null", + &interface, + ]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() - .context("unable to execute systemd-networkd")?; + .context("unable to execute /usr/sbin/dhcpcd")?; - Ok(Self { - config_path, - process, - }) + Ok(Self { process }) } } impl Drop for DHCPConfig { fn drop(&mut self) { - // Tell systemd-networkd to shutdown & wait for it to happen + // Tell dhcpcd to shutdown & wait for it to happen + info!("Sending SIGTERM to dhcpcd"); let _ = nix::sys::signal::kill(Pid::from_raw(self.process.id() as i32), SIGTERM); let _ = self.process.wait(); - - // Remove the config - let _ = std::fs::remove_file(&self.config_path); + info!("dhcpcd successfully stopped"); } } /// Tries to obtain the GuestOS config from the cloud's metadata service -pub fn obtain_guestos_config(systemd_network_dir: PathBuf) -> Result { +pub fn obtain_guestos_config() -> Result { + info!("Figuring out the network interface to use..."); + // Find the network interface to work on, it might not be initialized yet so give it a few tries - let mut retries = 10; + let mut retries = 30; let intf = loop { match get_best_interface_name() { Ok(v) => break v, Err(e) => { - info!("unable to choose interface: {e:#}"); + info!("Unable to choose interface: {e:#}"); + retries -= 1; if retries == 0 { bail!("unable to choose interface: retries exhausted"); @@ -83,10 +71,10 @@ pub fn obtain_guestos_config(systemd_network_dir: PathBuf) -> Result Result break v, + Err(e) => { + retries -= 1; + if retries == 0 { + bail!("unable to obtain config: retries exhausted"); + } + + info!("Unable to obtain GuestOS config (retries left: {retries}): {e:#}"); + std::thread::sleep(Duration::from_secs(1)); + } + }; + }; Ok(config) } diff --git a/rs/ic_os/os_tools/guestos_tool/src/main.rs b/rs/ic_os/os_tools/guestos_tool/src/main.rs index ddd06a5babc1..c27639b7f8b8 100644 --- a/rs/ic_os/os_tools/guestos_tool/src/main.rs +++ b/rs/ic_os/os_tools/guestos_tool/src/main.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use tracing::{info, warn}; +use tracing::{Level, info, warn}; use cloud_provision::obtain_guestos_config; use generate_network_config::{generate_networkd_config, validate_and_construct_ipv4_address_info}; @@ -55,10 +55,6 @@ pub enum Commands { /// Path where to save the obtained config.json #[arg(long, default_value = "/mnt/config/config.json")] config_path: PathBuf, - - /// systemd-networkd output directory - #[arg(long, default_value = DEFAULT_SYSTEMD_NETWORK_DIR)] - systemd_network_dir: PathBuf, }, } @@ -70,7 +66,7 @@ struct GuestOSArgs { } pub fn main() -> Result<()> { - ic_os_logging::init_logging(); + ic_os_logging::init_logging_with_level(Level::INFO); #[cfg(not(target_os = "linux"))] { @@ -118,12 +114,17 @@ pub fn main() -> Result<()> { Ok(()) } - Some(Commands::CloudProvision { - config_path, - systemd_network_dir, - }) => { - let config = obtain_guestos_config(systemd_network_dir) - .context("unable to obtain GuestOS config")?; + Some(Commands::CloudProvision { config_path }) => { + info!("Obtaining GuestOS config from the Cloud MDS..."); + + let config = match obtain_guestos_config().context("unable to obtain GuestOS config") { + Ok(v) => v, + Err(e) => { + info!("{e:#}"); + return Err(e); + } + }; + let json = serde_json::to_vec_pretty(&config).context("unable to encode to JSON")?; std::fs::write(&config_path, &json) .context("unable to write GuestOS config to disk")?; From bb0e2b915f3cb7dd4219f9eb04b52ead9539d9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Tackmann?= <54846571+Dfinity-Bjoern@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:38:45 +0200 Subject: [PATCH 15/75] feat: increase max environment variables per canister from 20 to 32 (#10435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Raises `MAX_ENVIRONMENT_VARIABLES` in `rs/config/src/execution_environment.rs` from 20 to 32. ## Test plan - [x] `rustfmt` clean - [x] `cargo clippy -p ic-config` clean - [x] `bazel build //rs/config:config` - [x] Ran directly-affected bazel tests (depth 2); env-var specific test (`test_environment_variable_system_api`) passes. Unrelated darwin-local flakes were observed in stable-memory / NNS / state_manager / consensus integration tests; none reference env vars. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Bjoern Tackmann Co-authored-by: Claude Opus 4.7 (1M context) --- rs/config/src/execution_environment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/config/src/execution_environment.rs b/rs/config/src/execution_environment.rs index 43686e93fe63..01406fc1e384 100644 --- a/rs/config/src/execution_environment.rs +++ b/rs/config/src/execution_environment.rs @@ -211,7 +211,7 @@ pub const MAX_CANISTER_HTTP_REQUESTS_IN_FLIGHT: usize = 3000; pub const DEFAULT_WASM_MEMORY_LIMIT: NumBytes = NumBytes::new(3 * GIB); /// The maximum number of environment variables allowed per canister. -pub const MAX_ENVIRONMENT_VARIABLES: usize = 20; +pub const MAX_ENVIRONMENT_VARIABLES: usize = 32; /// The maximum length of an environment variable name. pub const MAX_ENVIRONMENT_VARIABLE_NAME_LENGTH: usize = 128; From 02cccef383a834068d6e77b54c3d5d87cb516daf Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Thu, 11 Jun 2026 14:23:08 +0200 Subject: [PATCH 16/75] feat: bump bazel version to 9.1.1 (#10413) This bumps our bazel version to the latest available version. Some dependencies had to be bumped for 9.X support, but overall very little changed. --- .bazelversion | 2 +- MODULE.bazel | 30 ++++++++++++++----- bazel/conf/.bazelrc.build | 13 ++++++++ bazel/fuzz_testing.bzl | 7 +++++ bin/fuzzing/BUILD.bazel | 9 ++++++ bin/fuzzing/afl-clang-lto-linker.sh | 13 ++++++++ go.mod | 5 ++-- go.sum | 5 ++++ rs/http_endpoints/fuzz/BUILD.bazel | 15 ++++++++-- .../fuzz/fuzz_targets/execute_call_service.rs | 2 +- 10 files changed, 87 insertions(+), 14 deletions(-) create mode 100755 bin/fuzzing/afl-clang-lto-linker.sh diff --git a/.bazelversion b/.bazelversion index df5119ec64e6..44931da2660c 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -8.7.0 +9.1.1 diff --git a/MODULE.bazel b/MODULE.bazel index c0222949e967..1c4cd251711a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -17,8 +17,8 @@ bazel_dep(name = "rules_shell", version = "0.6.1") bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2", dev_dependency = True) # CC dependencies (for C libs like miracl-core, etc) -bazel_dep(name = "rules_cc", version = "0.2.16") -bazel_dep(name = "platforms", version = "1.0.0") +bazel_dep(name = "rules_cc", version = "0.2.17") +bazel_dep(name = "platforms", version = "1.1.0") bazel_dep(name = "hermetic_cc_toolchain", version = "4.1.0") single_version_override( module_name = "hermetic_cc_toolchain", @@ -51,7 +51,7 @@ archive_override( # Misc tools -bazel_dep(name = "pigz", version = "2.8") # (parallel) gzip +bazel_dep(name = "pigz", version = "2.8.bcr.1") # (parallel) gzip bazel_dep(name = "zstd", version = "1.5.7.bcr.1") # zstd can change format across versions # Rust @@ -60,7 +60,7 @@ include("//bazel:rust.MODULE.bazel") # Python dependencies -bazel_dep(name = "rules_python", version = "1.4.1") +bazel_dep(name = "rules_python", version = "1.7.0") python_version = "3.12" @@ -79,8 +79,13 @@ use_repo(pip, "python_deps") # toolchains_protoc must be declared before protobuf so its toolchain registration wins bazel_dep(name = "toolchains_protoc", version = "0.6.1") +# Pin protoc to the prebuilt binary toolchains_protoc provides (v33.0, its +# latest) and pin the `protobuf` module to the matching 33.0 below. protobuf 33+ +# enforces (via ProtocAuthenticityCheck) that protoc and the protobuf module +# share the same version, so they are kept in lockstep. +# https://registry.bazel.build/modules/toolchains_protoc protoc = use_extension("@toolchains_protoc//protoc:extensions.bzl", "protoc") -protoc.toolchain(version = "v32.1") +protoc.toolchain(version = "v33.0") use_repo( protoc, "toolchains_protoc_hub", @@ -94,7 +99,16 @@ register_toolchains("@toolchains_protoc_hub//:all") bazel_dep( name = "protobuf", - version = "32.1", + version = "33.0", +) + +# Bazel 9's builtin modules (rules_cc/rules_go/rules_python/...) pull in +# protobuf 33.4, but toolchains_protoc only ships a prebuilt protoc up to v33.0. +# Pin protobuf to 33.0 so it matches the prebuilt protoc and satisfies +# protobuf's ProtocAuthenticityCheck (33.0 and 33.4 are the same release line). +single_version_override( + module_name = "protobuf", + version = "33.0", ) # protobuf linter @@ -102,8 +116,8 @@ bazel_dep(name = "rules_buf", version = "0.5.2") # Go dependencies -bazel_dep(name = "rules_go", version = "0.50.1") -bazel_dep(name = "gazelle", version = "0.38.0") +bazel_dep(name = "rules_go", version = "0.61.1") +bazel_dep(name = "gazelle", version = "0.51.3") go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") go_deps.from_file(go_mod = "//:go.mod") diff --git a/bazel/conf/.bazelrc.build b/bazel/conf/.bazelrc.build index 79afac787fa0..11cdfa43a9ca 100644 --- a/bazel/conf/.bazelrc.build +++ b/bazel/conf/.bazelrc.build @@ -7,8 +7,21 @@ common --lockfile_mode=off # Use prebuilt protoc binary via toolchain resolution instead of compiling from C++ source. # See https://github.com/protocolbuffers/protobuf/issues/19558 +# (Removing this still builds, but reverts to compiling protoc/libprotoc from source.) common --incompatible_enable_proto_toolchain_resolution +# protobuf 33's upb bootstrap tools (protoc-gen-upb*, protoc_minimal, ...) use +# __start_/__stop_ linker-array sections (e.g. linkarr_upb_AllExts). lld (used by +# the hermetic zig toolchain on Linux) garbage-collects these under its default +# --start-stop-gc together with our opt-mode --gc-sections, breaking the link +# with "undefined symbol: __start_linkarr_upb_AllExts". The lld-suggested +# `-z nostart-stop-gc` is rejected by zig's wrapper, so we disable section GC +# instead. Scoped to the exec config (these are build-time `[for tool]` targets, +# so target binaries/canisters keep --gc-sections) and to Linux via +# --enable_platform_specific_config (the macOS system linker rejects this flag). +common --enable_platform_specific_config +build:linux --host_linkopt=-Wl,--no-gc-sections + # Use hermetic JDK # See https://bazel.build/docs/bazel-and-java#hermetic-testing build --java_runtime_version=remotejdk_17 diff --git a/bazel/fuzz_testing.bzl b/bazel/fuzz_testing.bzl index 50d948bd1dd7..f23fb857d213 100644 --- a/bazel/fuzz_testing.bzl +++ b/bazel/fuzz_testing.bzl @@ -102,6 +102,12 @@ def rust_fuzz_test_binary_afl(name, srcs, rustc_flags = [], crate_features = [], """ RUSTC_FLAGS_AFL = DEFAULT_RUSTC_FLAGS + [ + # afl-clang-lto is a Clang wrapper, so it understands -fsanitize=fuzzer + # and -fsanitize=address. We route through a thin wrapper script because + # rustc unconditionally injects -pass-exit-codes (a GCC-only flag) when + # using a gcc-flavor linker driver; the wrapper strips it before + # forwarding to afl-clang-lto. + "-Clinker=$(location //bin/fuzzing:afl_clang_lto_linker)", "-Cllvm-args=-sanitizer-coverage-trace-pc-guard", "-Clink-arg=-fuse-ld=lld", "-Clink-arg=-fsanitize=fuzzer", @@ -122,6 +128,7 @@ def rust_fuzz_test_binary_afl(name, srcs, rustc_flags = [], crate_features = [], crate_features = crate_features + ["fuzzing"], proc_macro_deps = proc_macro_deps, deps = deps, + compile_data = ["//bin/fuzzing:afl_clang_lto_linker"], rustc_flags = rustc_flags + RUSTC_FLAGS_AFL, tags = [ # Makes sure this target is not run in normal CI builds. It would fail due to non-nightly Rust toolchain. diff --git a/bin/fuzzing/BUILD.bazel b/bin/fuzzing/BUILD.bazel index c2f59d33492b..0c40feb3f38f 100644 --- a/bin/fuzzing/BUILD.bazel +++ b/bin/fuzzing/BUILD.bazel @@ -10,3 +10,12 @@ sh_binary( "fuzz_test", ], ) + +sh_binary( + name = "afl_clang_lto_linker", + srcs = ["afl-clang-lto-linker.sh"], + tags = [ + "afl", + "fuzz_test", + ], +) diff --git a/bin/fuzzing/afl-clang-lto-linker.sh b/bin/fuzzing/afl-clang-lto-linker.sh new file mode 100755 index 000000000000..1b822c58da7e --- /dev/null +++ b/bin/fuzzing/afl-clang-lto-linker.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Wrapper around afl-clang-lto used as Rust's linker driver (-Clinker=...). +# Rustc unconditionally passes -pass-exit-codes when it detects a gcc-flavor +# linker, but that flag is GCC-specific and rejected by Clang. Strip it here +# before forwarding the rest of the arguments to afl-clang-lto. +args=() +for arg in "$@"; do + case "$arg" in + -pass-exit-codes) ;; + *) args+=("$arg") ;; + esac +done +exec afl-clang-lto "${args[@]}" diff --git a/go.mod b/go.mod index 07cc5f77e592..cd823cf6ec33 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,10 @@ go 1.19 require ( github.com/fatih/color v1.13.0 - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.6.0 github.com/schollz/closestmatch v2.1.0+incompatible github.com/spf13/cobra v1.6.1 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.4 ) require ( @@ -17,6 +17,7 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bfec957245fb..6152f270ea65 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -26,11 +28,14 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/rs/http_endpoints/fuzz/BUILD.bazel b/rs/http_endpoints/fuzz/BUILD.bazel index 68909cdd6bd0..13999c863835 100644 --- a/rs/http_endpoints/fuzz/BUILD.bazel +++ b/rs/http_endpoints/fuzz/BUILD.bazel @@ -2,14 +2,25 @@ load("//bazel:fuzz_testing.bzl", "rust_fuzz_test_binary_afl") package(default_visibility = ["//visibility:private"]) -# required to compile tests/common +# Copy common/mod.rs from the public-tests package into this package's +# bazel-out directory so that the #[path] attribute in execute_call_service.rs +# can reach it via a relative path that stays within bazel-out. +# (Listing the source file directly in srcs only makes it accessible at its +# source-tree path, but execute_call_service.rs is compiled from a bazel-out +# directory and its #[path = "../fuzz_test_common.rs"] resolves there.) +genrule( + name = "http_endpoint_test_common", + srcs = ["//rs/http_endpoints/public:tests/common/mod.rs"], + outs = ["fuzz_test_common.rs"], + cmd = "cp $< $@", +) rust_fuzz_test_binary_afl( name = "execute_call_service_afl", testonly = True, srcs = [ "fuzz_targets/execute_call_service.rs", - "//rs/http_endpoints/public:tests/common/mod.rs", + ":http_endpoint_test_common", ], crate_root = "fuzz_targets/execute_call_service.rs", deps = [ diff --git a/rs/http_endpoints/fuzz/fuzz_targets/execute_call_service.rs b/rs/http_endpoints/fuzz/fuzz_targets/execute_call_service.rs index 29dce19df530..e0f5bb5dd3dd 100644 --- a/rs/http_endpoints/fuzz/fuzz_targets/execute_call_service.rs +++ b/rs/http_endpoints/fuzz/fuzz_targets/execute_call_service.rs @@ -32,7 +32,7 @@ use tower::{ }; use tower_test::mock::Handle; -#[path = "../../public/tests/common/mod.rs"] +#[path = "../fuzz_test_common.rs"] pub mod common; use common::{basic_registry_client, get_free_localhost_socket_addr, setup_ingress_filter_mock}; From 0cab8023bf0c4f1ae442b5d90bf4c09fcfb9d626 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Thu, 11 Jun 2026 15:12:01 +0200 Subject: [PATCH 17/75] fix: deflake //rs/tests/networking:canister_http_* tests (#10441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Root Cause The `//rs/tests/networking:canister_http_*` tests fail in `setup` with: ``` Orchestrator rule did not appear in time.: Timed out waiting for orchestrator rule. ... Output: Err: Error: No such file or directory list chain ip6 filter OUTPUT ``` `canister_http::setup` → `start_httpbin_on_uvm` → `wait_for_orchestrator_fw_rules` polls every node for the orchestrator-applied `ip6 filter OUTPUT` chain. In the analyzed failure the **entire `ip6 filter` table** was missing on one node and never appeared during the whole 60s retry window — long after all replicas reported healthy. That's a *permanent* condition, not a slow one. The GuestOS firewall application pipeline is: 1. The orchestrator generates the nftables ruleset and writes it to `/run/ic-node/nftables-ruleset/nftables.conf` — but **only when its content changes** (or on the very first check after start). 2. `reload_nftables.path` (`PathChanged=`) triggers `reload_nftables.service`, which ran: - `ExecStartPre=/usr/sbin/nft flush ruleset` - `ExecStart=systemctl reload nftables.service` (→ `nft -f /etc/nftables.conf`) This sequence is **neither atomic nor retried**: - The flush runs *outside* the nft transaction. If the subsequent load fails for any transient reason, the node is left with an **empty ruleset**. - The canonical transient failure is hostname resolution of `hostos` in the template rule `ip6 saddr { hostos } ct state { new } tcp dport { 42372 } accept`, which is resolved at `nft` load time (via the `nss_icos` NSS plugin, falling through to DNS) and can fail with `Error: Could not resolve hostname: Temporary failure in name resolution` early after boot. - Nothing retries: the service is `Type=oneshot` without restart, and the path unit only re-fires when the orchestrator rewrites the file — which it effectively never does again on a short-lived testnet. A single transient `nft` load failure therefore permanently disables the firewall on that node, and the test times out waiting for a chain that will never exist. The `canister_http_*` family is the canary because it's the only test family that explicitly waits for this rule on **every** node of its testnet. PR #9857 previously narrowed this race by adding `After=nss-lookup.target` ordering to `reload_nftables.service`, but `After=` is pure start ordering — by the time the path unit fires, those targets are long active, so it doesn't guarantee that resolving `hostos` succeeds. The race window was narrowed, not closed. ## Fix Make the firewall apply **atomic** and **retried**: - Prepend `flush ruleset` to the orchestrator-generated nftables config (both the replica and the boundary-node firewall templates in `ic.json5.template`), so flushing the old ruleset and loading the new one happen in a single `nft` transaction. A failed load now leaves the previously active ruleset in effect instead of leaving the node without a firewall. - In `reload_nftables.service`, drop the non-atomic `ExecStartPre` flush, run `nft -f /etc/nftables.conf` directly, and add `Restart=on-failure` / `RestartSec=2` (bounded by `StartLimitIntervalSec=300` / `StartLimitBurst=30`, i.e. ~60s of retries). Since the load is atomic, retrying is safe. If it keeps failing, the unit ends up in a failed state, making the problem visible (e.g. to `//rs/tests/node:guestos_no_failed_systemd_units`). The 6 nftables golden files are updated accordingly. ## Verification - `bazel test //rs/orchestrator:orchestrator_test` (nftables golden tests) passes. - `bazel test --runs_per_test=//rs/tests/networking:canister_http_test@3 //rs/tests/networking:canister_http_test //rs/tests/node:guestos_no_failed_systemd_units` --- This PR was created following the steps in `.claude/skills/fix-flaky-tests/SKILL.md`. --- .../nftables/reload_nftables.service | 23 +++++++++++++++---- .../config/tool/templates/ic.json5.template | 8 +++++-- ...nftables_assigned_cloud_engine.conf.golden | 2 ++ .../nftables_assigned_replica.conf.golden | 2 ++ ...ables_boundary_node_app_subnet.conf.golden | 2 ++ ...es_boundary_node_system_subnet.conf.golden | 2 ++ ...tables_unassigned_cloud_engine.conf.golden | 2 ++ .../nftables_unassigned_replica.conf.golden | 2 ++ 8 files changed, 37 insertions(+), 6 deletions(-) diff --git a/ic-os/components/networking/nftables/reload_nftables.service b/ic-os/components/networking/nftables/reload_nftables.service index 3a3639bcbb62..ddc975b099ab 100644 --- a/ic-os/components/networking/nftables/reload_nftables.service +++ b/ic-os/components/networking/nftables/reload_nftables.service @@ -2,12 +2,27 @@ Description=Reload nftables when the configuration changes # The orchestrator-generated nftables config uses the hostname "hostos" which # is resolved via the nss_icos NSS plugin. We must wait for name resolution to -# be available, otherwise the nft reload fails with "Could not resolve hostname" -# and the entire ruleset is discarded (nft is transactional). +# be available, otherwise the nft load fails with "Could not resolve hostname". After=nss-lookup.target systemd-resolved.service Wants=nss-lookup.target +# Allow the restarts configured below (Restart=on-failure with RestartSec=2) +# to go on for up to ~60s before giving up and marking the unit as failed. +StartLimitIntervalSec=300 +StartLimitBurst=30 [Service] Type=oneshot -ExecStartPre=/usr/sbin/nft flush ruleset -ExecStart=systemctl reload nftables.service +# The orchestrator-generated config starts with "flush ruleset" so flushing +# the old ruleset and loading the new one happen in a single nft transaction: +# if loading fails the previously active ruleset stays in effect instead of +# leaving the node without a firewall (nft -f is transactional). +# Note: /etc/nftables.conf is a symlink to the file loaded below (see the +# GuestOS Dockerfile), so nftables.service at boot loads the same config. +ExecStart=/usr/sbin/nft -f /run/ic-node/nftables-ruleset/nftables.conf +# Loading can still fail transiently, e.g. when the "hostos" hostname cannot +# be resolved yet early during boot. Since the load is atomic it is safe to +# simply retry until it succeeds. If it keeps failing, the unit ends up in a +# failed state, making the problem visible (e.g. to +# //rs/tests/node:guestos_no_failed_systemd_units). +Restart=on-failure +RestartSec=2 diff --git a/rs/ic_os/config/tool/templates/ic.json5.template b/rs/ic_os/config/tool/templates/ic.json5.template index 04a54de3195c..8a863eaac3e8 100644 --- a/rs/ic_os/config/tool/templates/ic.json5.template +++ b/rs/ic_os/config/tool/templates/ic.json5.template @@ -184,7 +184,9 @@ firewall: { config_file: "/run/ic-node/nftables-ruleset/nftables.conf", - file_template: "table filter {\n\ + file_template: "flush ruleset\n\ +\n\ +table filter {\n\ define icmp_v4_types_accept = {\n\ destination-unreachable,\n\ time-exceeded,\n\ @@ -331,7 +333,9 @@ table ip6 filter {\n\ boundary_node_firewall: { config_file: "/run/ic-node/nftables-ruleset/nftables.conf", - file_template: "table filter {\n\ + file_template: "flush ruleset\n\ +\n\ +table filter {\n\ set rate_limit {\n\ type ipv4_addr\n\ size 65535\n\ diff --git a/rs/orchestrator/testdata/nftables_assigned_cloud_engine.conf.golden b/rs/orchestrator/testdata/nftables_assigned_cloud_engine.conf.golden index 9bf57a403697..c15ea8397ac8 100644 --- a/rs/orchestrator/testdata/nftables_assigned_cloud_engine.conf.golden +++ b/rs/orchestrator/testdata/nftables_assigned_cloud_engine.conf.golden @@ -1,3 +1,5 @@ +flush ruleset + table filter { define icmp_v4_types_accept = { destination-unreachable, diff --git a/rs/orchestrator/testdata/nftables_assigned_replica.conf.golden b/rs/orchestrator/testdata/nftables_assigned_replica.conf.golden index d9ae8896f6bf..9c70a94fe706 100644 --- a/rs/orchestrator/testdata/nftables_assigned_replica.conf.golden +++ b/rs/orchestrator/testdata/nftables_assigned_replica.conf.golden @@ -1,3 +1,5 @@ +flush ruleset + table filter { define icmp_v4_types_accept = { destination-unreachable, diff --git a/rs/orchestrator/testdata/nftables_boundary_node_app_subnet.conf.golden b/rs/orchestrator/testdata/nftables_boundary_node_app_subnet.conf.golden index ee516706f637..bf1446321a42 100644 --- a/rs/orchestrator/testdata/nftables_boundary_node_app_subnet.conf.golden +++ b/rs/orchestrator/testdata/nftables_boundary_node_app_subnet.conf.golden @@ -1,3 +1,5 @@ +flush ruleset + table filter { set rate_limit { type ipv4_addr diff --git a/rs/orchestrator/testdata/nftables_boundary_node_system_subnet.conf.golden b/rs/orchestrator/testdata/nftables_boundary_node_system_subnet.conf.golden index 7f2bb9660281..b67e861502b5 100644 --- a/rs/orchestrator/testdata/nftables_boundary_node_system_subnet.conf.golden +++ b/rs/orchestrator/testdata/nftables_boundary_node_system_subnet.conf.golden @@ -1,3 +1,5 @@ +flush ruleset + table filter { set rate_limit { type ipv4_addr diff --git a/rs/orchestrator/testdata/nftables_unassigned_cloud_engine.conf.golden b/rs/orchestrator/testdata/nftables_unassigned_cloud_engine.conf.golden index 08ab9b7900ab..c816c5e3ba9b 100644 --- a/rs/orchestrator/testdata/nftables_unassigned_cloud_engine.conf.golden +++ b/rs/orchestrator/testdata/nftables_unassigned_cloud_engine.conf.golden @@ -1,3 +1,5 @@ +flush ruleset + table filter { define icmp_v4_types_accept = { destination-unreachable, diff --git a/rs/orchestrator/testdata/nftables_unassigned_replica.conf.golden b/rs/orchestrator/testdata/nftables_unassigned_replica.conf.golden index 4b6d6c5aa148..ef1c0e4869e8 100644 --- a/rs/orchestrator/testdata/nftables_unassigned_replica.conf.golden +++ b/rs/orchestrator/testdata/nftables_unassigned_replica.conf.golden @@ -1,3 +1,5 @@ +flush ruleset + table filter { define icmp_v4_types_accept = { destination-unreachable, From 56007f69a7dd6586ca0613a057cc571d061e00ca Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:01:10 +0200 Subject: [PATCH 18/75] fix: disable canister log memory store feature (#10444) Reverts https://github.com/dfinity/ic/pull/10160 --- rs/config/src/execution_environment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/config/src/execution_environment.rs b/rs/config/src/execution_environment.rs index 01406fc1e384..75a72fc8aacc 100644 --- a/rs/config/src/execution_environment.rs +++ b/rs/config/src/execution_environment.rs @@ -14,7 +14,7 @@ const TIB: u64 = 1024 * GIB; const REPLICATED_INTER_CANISTER_LOG_FETCH_FEATURE: FlagStatus = FlagStatus::Disabled; // TODO(DSM-105): remove after the feature is enabled by default. -pub const LOG_MEMORY_STORE_FEATURE_ENABLED: bool = true; +pub const LOG_MEMORY_STORE_FEATURE_ENABLED: bool = false; pub const LOG_MEMORY_STORE_FEATURE: FlagStatus = if LOG_MEMORY_STORE_FEATURE_ENABLED { FlagStatus::Enabled } else { From 5a786366e559bfeac278444e181a9fc15a10c1c6 Mon Sep 17 00:00:00 2001 From: "pr-creation-bot-dfinity-ic[bot]" <200595415+pr-creation-bot-dfinity-ic[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:23:05 -0700 Subject: [PATCH 19/75] chore: Update Base Image Refs [2026-06-11-0925] (#10437) Updating base container image references. Run URL: https://github.com/dfinity/ic/actions/runs/27337008974 Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> --- ic-os/bootloader/context/docker-base | 2 +- ic-os/guestos/context/docker-base.dev | 2 +- ic-os/guestos/context/docker-base.prod | 2 +- ic-os/hostos/context/docker-base.dev | 2 +- ic-os/hostos/context/docker-base.prod | 2 +- ic-os/setupos/context/docker-base.dev | 2 +- ic-os/setupos/context/docker-base.prod | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ic-os/bootloader/context/docker-base b/ic-os/bootloader/context/docker-base index a60bf4bf9455..cf2db8e15f01 100644 --- a/ic-os/bootloader/context/docker-base +++ b/ic-os/bootloader/context/docker-base @@ -1 +1 @@ -ghcr.io/dfinity/bootloader-base@sha256:1c037b4d56001387871f065acfc49f40d87ed6ef51e71ea44a6cc4785a5ac87c +ghcr.io/dfinity/bootloader-base@sha256:1c4865120a7f69145a0266091e05e3385246c7ada3bb2d747798a2e4f51735f9 diff --git a/ic-os/guestos/context/docker-base.dev b/ic-os/guestos/context/docker-base.dev index 8a8f7923ca2d..5d624ee4e8ca 100644 --- a/ic-os/guestos/context/docker-base.dev +++ b/ic-os/guestos/context/docker-base.dev @@ -1 +1 @@ -ghcr.io/dfinity/guestos-base-dev@sha256:8da22191e8fb70eba385f67fede165f3bd2a1c06498ad9da271ff7afd965b7ab +ghcr.io/dfinity/guestos-base-dev@sha256:d6695ef09829496c88e7903f7d5c20e452625f50e8befa2954b6b5f6bc36c8a4 diff --git a/ic-os/guestos/context/docker-base.prod b/ic-os/guestos/context/docker-base.prod index 481c59803750..e6b6fff6f9f0 100644 --- a/ic-os/guestos/context/docker-base.prod +++ b/ic-os/guestos/context/docker-base.prod @@ -1 +1 @@ -ghcr.io/dfinity/guestos-base@sha256:80a1dbcbbe2cfc430f43fa3e9449816eebb02166dcb79f0cfedc4efef7cb7378 +ghcr.io/dfinity/guestos-base@sha256:ae4b55e9ac8f81a3a8a177d1a22ae24296b68cc6fc12651713c5121f89a596d2 diff --git a/ic-os/hostos/context/docker-base.dev b/ic-os/hostos/context/docker-base.dev index 8918b2af406f..22160d0434cf 100644 --- a/ic-os/hostos/context/docker-base.dev +++ b/ic-os/hostos/context/docker-base.dev @@ -1 +1 @@ -ghcr.io/dfinity/hostos-base-dev@sha256:e7da7fa4d19e1e79fb8cafb8fa88a609603410e2a17c1ab45661072708f51b2f +ghcr.io/dfinity/hostos-base-dev@sha256:056e3249bb98d6351d2bb29f7e8d213ed51ec38b58e17d865647ad612fb3beaa diff --git a/ic-os/hostos/context/docker-base.prod b/ic-os/hostos/context/docker-base.prod index 5e534f10a221..dbaaa0694476 100644 --- a/ic-os/hostos/context/docker-base.prod +++ b/ic-os/hostos/context/docker-base.prod @@ -1 +1 @@ -ghcr.io/dfinity/hostos-base@sha256:52eeaf8344f46f6a9202fed0a5c405d7825614ee3361802da29e3ff25a5ca16e +ghcr.io/dfinity/hostos-base@sha256:63804a7856c583d776753d361485a71ca7bc314b6c0e6ba42f25dd673935900f diff --git a/ic-os/setupos/context/docker-base.dev b/ic-os/setupos/context/docker-base.dev index 90d710b54476..a03f84b9fd8d 100644 --- a/ic-os/setupos/context/docker-base.dev +++ b/ic-os/setupos/context/docker-base.dev @@ -1 +1 @@ -ghcr.io/dfinity/setupos-base-dev@sha256:43bf96063ebf587a97374b224d79f813a98f3e30116164e0b0ce3b983f8fc2c5 +ghcr.io/dfinity/setupos-base-dev@sha256:bca2fb8755eb1ec78414f69ecf23509e295248f201b26ab6de01faa1092ca3ca diff --git a/ic-os/setupos/context/docker-base.prod b/ic-os/setupos/context/docker-base.prod index a235aa8e66ea..e83cd2379820 100644 --- a/ic-os/setupos/context/docker-base.prod +++ b/ic-os/setupos/context/docker-base.prod @@ -1 +1 @@ -ghcr.io/dfinity/setupos-base@sha256:cf70a4137f0670e20af7638a448d9505808a5ec83b54f314765472a73fbf71f5 +ghcr.io/dfinity/setupos-base@sha256:91869212140e01d33b0a9f82cf1ced7e062a822c6fc094578d5b9d8d94e573e7 From 4258218f3fddcf2de606325bb275676d6dfd8ca4 Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Thu, 11 Jun 2026 19:26:59 +0200 Subject: [PATCH 20/75] feat: hoist dosfstools and mtools out of Dockerfile (#10439) This moves two further IC-OS tools, dosfstools and mtools, out of the Dockerfile and into the Bazel build. This is similar to what we did with e2fsprogs. This makes the build more portable. See also: https://github.com/dfinity/ic/pull/10369 --- BUILD.bazel | 30 +++++++++++++ MODULE.bazel | 19 +++++++++ bazel/defs.bzl | 16 ++++++- rs/tests/system_tests.bzl | 10 ++++- third_party/BUILD.dosfstools.bazel | 45 ++++++++++++++++++++ third_party/BUILD.mtools.bazel | 54 ++++++++++++++++++++++++ toolchains/sysimage/build_fat32_image.py | 53 +++++++++++------------ toolchains/sysimage/build_vfat_image.py | 48 +++++++++------------ toolchains/sysimage/toolchain.bzl | 48 +++++++++++++++++++++ 9 files changed, 262 insertions(+), 61 deletions(-) create mode 100644 third_party/BUILD.dosfstools.bazel create mode 100644 third_party/BUILD.mtools.bazel diff --git a/BUILD.bazel b/BUILD.bazel index c489489cb205..ef16436f8d27 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -218,6 +218,36 @@ alias( }), ) +### dosfstools, used to build (mkfs.fat) and label (fatlabel) FAT filesystem images + +# Both aliases resolve to the dosfstools package, which builds the mkfs.fat and +# fatlabel binaries; consumers pick the binary they need by basename. +alias( + name = "mkfs.fat", + actual = select({ + "@bazel_tools//src/conditions:linux_x86_64": "@dosfstools//:dosfstools", + "//conditions:default": "@platforms//:incompatible", + }), +) + +alias( + name = "fatlabel", + actual = select({ + "@bazel_tools//src/conditions:linux_x86_64": "@dosfstools//:dosfstools", + "//conditions:default": "@platforms//:incompatible", + }), +) + +### mtools, used to populate FAT filesystem images (invoked as `mtools -c mcopy/mmd`) + +alias( + name = "mtools", + actual = select({ + "@bazel_tools//src/conditions:linux_x86_64": "@mtools//:mtools_build", + "//conditions:default": "@platforms//:incompatible", + }), +) + ### shfmt, used to format bash code alias( diff --git a/MODULE.bazel b/MODULE.bazel index 1c4cd251711a..41040c5ef31a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -723,6 +723,25 @@ http_archive( urls = ["https://www.kernel.org/pub/linux/kernel/people/tytso/e2fsprogs/v1.47.4/e2fsprogs-1.47.4.tar.gz"], ) +http_archive( + name = "dosfstools", + build_file = "@//third_party:BUILD.dosfstools.bazel", + sha256 = "64926eebf90092dca21b14259a5301b7b98e7b1943e8a201c7d726084809b527", + strip_prefix = "dosfstools-4.2", + urls = ["https://github.com/dosfstools/dosfstools/releases/download/v4.2/dosfstools-4.2.tar.gz"], +) + +http_archive( + name = "mtools", + build_file = "@//third_party:BUILD.mtools.bazel", + sha256 = "10cd1111da87bf2400a380c1639a6cba8bfb937a24f9c51f5f88d393ae5f6f76", + strip_prefix = "mtools-4.0.49", + urls = [ + "https://ftp.gnu.org/gnu/mtools/mtools-4.0.49.tar.gz", + "https://ftpmirror.gnu.org/mtools/mtools-4.0.49.tar.gz", + ], +) + http_archive( name = "lmdb", build_file = "@//third_party:BUILD.lmdb.bazel", diff --git a/bazel/defs.bzl b/bazel/defs.bzl index 6581e3eef71b..881cc5d7319c 100644 --- a/bazel/defs.bzl +++ b/bazel/defs.bzl @@ -86,6 +86,16 @@ def _mcopy(ctx): """ out = ctx.actions.declare_file(ctx.label.name) + # //:mtools resolves to the mtools bundle (the mtools binary + an include + # dir); pick out the binary, which we drive as `mtools -c mcopy ...`. + mtools = None + for f in ctx.files._mtools: + if f.basename == "mtools": + mtools = f + break + if not mtools: + fail("could not locate mtools binary among //:mtools outputs") + command = "cp -p {fs} {output} && chmod +w {output} ".format(fs = ctx.file.fs.path, output = out.path) inputs = [] for srcs, dest in ctx.attr.srcmap.items(): @@ -96,7 +106,8 @@ def _mcopy(ctx): dest_path = dest + src_file.basename else: dest_path = dest - command += "&& mcopy -mi {output} -sQ {src_path} ::/{dest} ".format( + command += "&& {mtools} -c mcopy -mi {output} -sQ {src_path} ::/{dest} ".format( + mtools = mtools.path, output = out.path, src_path = src_file.path, dest = dest_path.removeprefix("/"), @@ -104,7 +115,7 @@ def _mcopy(ctx): ctx.actions.run_shell( command = command, - inputs = inputs + [ctx.file.fs], + inputs = inputs + [ctx.file.fs] + ctx.files._mtools, outputs = [out], ) return [DefaultInfo(files = depset([out]), runfiles = ctx.runfiles(files = [out]))] @@ -114,6 +125,7 @@ mcopy = rule( attrs = { "srcmap": attr.label_keyed_string_dict(allow_files = True), "fs": attr.label(allow_single_file = True), + "_mtools": attr.label(default = "//:mtools", cfg = "exec", allow_files = True), }, ) diff --git a/rs/tests/system_tests.bzl b/rs/tests/system_tests.bzl index 30cb77f6ff32..16adb7268d0c 100644 --- a/rs/tests/system_tests.bzl +++ b/rs/tests/system_tests.bzl @@ -361,14 +361,20 @@ def uvm_config_image(name, tags = None, visibility = None, srcmap = None, teston testonly = testonly, ) - # TODO: install dosfstools as dependency native.genrule( name = name + "_vfat", srcs = [":" + name + "_size"], outs = [name + "_vfat.img"], + tools = ["//:mkfs.fat"], + # //:mkfs.fat resolves to the dosfstools bundle (mkfs.fat + fatlabel), + # so pick out the mkfs.fat binary by name. cmd = """ + mkfs_fat= + for f in $(locations //:mkfs.fat); do + case "$$f" in */mkfs.fat) mkfs_fat="$$f" ;; esac + done truncate -s $$(cat $<) $@ - /usr/sbin/mkfs.vfat -i "0" -n CONFIG $@ + "$$mkfs_fat" -i "0" -n CONFIG $@ """, tags = ["manual"], target_compatible_with = ["@platforms//os:linux"], diff --git a/third_party/BUILD.dosfstools.bazel b/third_party/BUILD.dosfstools.bazel new file mode 100644 index 000000000000..a7396bf63991 --- /dev/null +++ b/third_party/BUILD.dosfstools.bazel @@ -0,0 +1,45 @@ +""" +dosfstools, built from source. Provides mkfs.fat (used to create FAT/VFAT +filesystem images for IC-OS) and fatlabel (used to set their volume label). +""" + +load("@rules_foreign_cc//foreign_cc:defs.bzl", "configure_make") + +filegroup( + name = "all_srcs", + srcs = glob(["**"]), +) + +configure_make( + name = "dosfstools", + args = ["-j"], + configure_in_place = True, + configure_options = [ + # Build without iconv: dosfstools then uses its compiled-in CP850 + # conversion table instead of glibc's iconv/gconv modules. IC-OS only + # uses ASCII volume labels (covered by CP850), and this keeps the build + # hermetic (no dependency on which gconv modules the runtime glibc ships) + # and self-contained. + "--without-iconv", + ], + lib_source = ":all_srcs", + out_binaries = [ + "mkfs.fat", + "fatlabel", + ], + # We don't ship any libs out of this build. + out_shared_libs = [], + out_static_libs = [], + # mkfs.fat and fatlabel install under sbin/ by autotools (they're sysadmin + # tools); rules_foreign_cc only picks up out_binaries from bin/, so move them. + postfix_script = ( + "mkdir -p $$INSTALLDIR/bin $$INSTALLDIR/include && " + + "cp $$INSTALLDIR/sbin/mkfs.fat $$INSTALLDIR/bin/mkfs.fat && " + + "cp $$INSTALLDIR/sbin/fatlabel $$INSTALLDIR/bin/fatlabel" + ), + target_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + visibility = ["//visibility:public"], +) diff --git a/third_party/BUILD.mtools.bazel b/third_party/BUILD.mtools.bazel new file mode 100644 index 000000000000..4646d24c3175 --- /dev/null +++ b/third_party/BUILD.mtools.bazel @@ -0,0 +1,54 @@ +""" +mtools, built from source. Provides the `mtools` multi-call binary; IC-OS uses +it as `mtools -c mcopy ...` and `mtools -c mmd ...` to populate FAT/VFAT images. + +Built without iconv (see ac_cv_header_iconv_h below) so mtools uses its +compiled-in codepage tables (default CP850) rather than glibc's gconv modules: +IC-OS only uses ASCII file names, and this keeps the binary self-contained. +""" + +load("@rules_foreign_cc//foreign_cc:defs.bzl", "configure_make") + +filegroup( + name = "all_srcs", + srcs = glob(["**"]), +) + +# NB: the target is intentionally *not* named "mtools": with configure_in_place +# the build tree and the install dir (named after the target) share a parent, and +# mtools links its "mtools" binary at the source-tree top level, so a target named +# "mtools" collides with the install dir ("cannot open output file mtools: Is a +# directory"). +configure_make( + name = "mtools_build", + args = ["-j"], + configure_in_place = True, + # Pin sysconfdir to a fixed, build-location-independent path. mtools compiles + # its system config-file location in as SYSCONFDIR "/mtools.conf"; rules_foreign_cc + # points configure's --prefix at the per-build $BUILD_TMPDIR, so the default + # sysconfdir ($prefix/etc) bakes that absolute sandbox path into the binary's + # .rodata. Two builds at different output bases then differ, breaking + # reproducibility. /etc is the conventional location (mtools already reads + # /etc/default/mtools.conf) and mtools installs nothing there, so this is the + # build-independent path without changing behavior: IC-OS's hermetic + # `mtools -c mcopy/mmd` invocations don't ship an /etc/mtools.conf, so mtools + # falls back to its compiled-in defaults either way. (Cf. e2fsprogs, which + # drops the analogous ROOT_SYSCONFDIR path baked into mke2fs.) + configure_options = ["--sysconfdir=/etc"], + # Pretend iconv.h is absent so configure leaves HAVE_ICONV_H undefined and + # mtools compiles in its built-in codepage tables instead of calling iconv. + env = {"ac_cv_header_iconv_h": "no"}, + lib_source = ":all_srcs", + out_binaries = ["mtools"], + # We don't ship any libs out of this build. + out_shared_libs = [], + out_static_libs = [], + # mtools installs to bin/ already; just make sure the include output dir + # that rules_foreign_cc expects exists (mtools installs no headers). + postfix_script = "mkdir -p $$INSTALLDIR/include", + target_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + visibility = ["//visibility:public"], +) diff --git a/toolchains/sysimage/build_fat32_image.py b/toolchains/sysimage/build_fat32_image.py index fe95dcd75bb5..324eded78da9 100755 --- a/toolchains/sysimage/build_fat32_image.py +++ b/toolchains/sysimage/build_fat32_image.py @@ -17,7 +17,15 @@ from toolchains.sysimage.utils import parse_size -def untar_to_fat32(tf, fs_basedir, out_file, path_transform): +def mtools_cmd(mtools, subcommand, *extra_args): + # mtools is a multi-call binary; invoke a subcommand (mmd, mcopy, ...) via + # `mtools -c `. Wrapped in faketime for deterministic timestamps; + # the binary path is made absolute so faketime (which execs it) treats it as + # a path rather than searching PATH. + return ["faketime", "-f", "1970-1-1 0:0:0", os.path.abspath(mtools), "-c", subcommand, *extra_args] + + +def untar_to_fat32(tf, fs_basedir, out_file, path_transform, mtools): """ Put contents of tarfile into fat32 image. @@ -39,45 +47,25 @@ def untar_to_fat32(tf, fs_basedir, out_file, path_transform): if path == "": continue os.mkdir(os.path.join(fs_basedir, path)) - subprocess.run(["faketime", "-f", "1970-1-1 0:0:0", "mmd", "-i", out_file, "::/" + path], check=True) + subprocess.run(mtools_cmd(mtools, "mmd", "-i", out_file, "::/" + path), check=True) elif member.type == tarfile.REGTYPE or member.type == tarfile.AREGTYPE: with open(os.path.join(fs_basedir, path), "wb") as f: f.write(tf.extractfile(member).read()) subprocess.run( - [ - "faketime", - "-f", - "1970-1-1 0:0:0", - "mcopy", - "-o", - "-i", - out_file, - os.path.join(fs_basedir, path), - "::/" + path, - ], + mtools_cmd(mtools, "mcopy", "-o", "-i", out_file, os.path.join(fs_basedir, path), "::/" + path), check=True, ) else: raise RuntimeError("Unhandled tar member kind: %s" % member.type) -def install_extra_files(out_file, extra_files, path_transform): +def install_extra_files(out_file, extra_files, path_transform, mtools): for extra_file in extra_files: source_file, install_target, mode = extra_file.split(":") if install_target[0] == "/": install_target = install_target[1:] subprocess.run( - [ - "faketime", - "-f", - "1970-1-1 0:0:0", - "mcopy", - "-o", - "-i", - out_file, - source_file, - "::/" + path_transform(install_target), - ], + mtools_cmd(mtools, "mcopy", "-o", "-i", out_file, source_file, "::/" + path_transform(install_target)), check=True, ) @@ -107,6 +95,9 @@ def main(): ) parser.add_argument("--dflate", help="Path to our dflate tool", type=str) parser.add_argument("--zstd", help="Path to the zstd tool", type=str) + parser.add_argument("--mkfs-fat", help="Path to the mkfs.fat tool", type=str, required=True) + parser.add_argument("--fatlabel", help="Path to the fatlabel tool", type=str, required=True) + parser.add_argument("--mtools", help="Path to the mtools tool", type=str, required=True) args = parser.parse_args(sys.argv[1:]) @@ -132,15 +123,19 @@ def path_transform(path, limit_prefix=limit_prefix): os.close(os.open(image_file, os.O_CREAT | os.O_RDWR | os.O_CLOEXEC | os.O_EXCL, 0o600)) os.truncate(image_file, image_size) - subprocess.run(["/usr/sbin/mkfs.fat", "-F", "32", "-i", "0", image_file], check=True) + subprocess.run([os.path.abspath(args.mkfs_fat), "-F", "32", "-i", "0", image_file], check=True) if image_label: - subprocess.run(["faketime", "-f", "1970-1-1 0:0:0", "/usr/sbin/fatlabel", image_file, image_label], check=True) + # Absolute path so faketime (which execs it) resolves it as a path + # rather than searching PATH. + subprocess.run( + ["faketime", "-f", "1970-1-1 0:0:0", os.path.abspath(args.fatlabel), image_file, image_label], check=True + ) if in_file: with tarfile.open(in_file, mode="r|*") as tf: - untar_to_fat32(tf, fs_basedir, image_file, path_transform) + untar_to_fat32(tf, fs_basedir, image_file, path_transform, args.mtools) - install_extra_files(image_file, extra_files, path_transform) + install_extra_files(image_file, extra_files, path_transform, args.mtools) # We use our tool, dflate, to quickly create a sparse, deterministic, tar. # If dflate is ever misbehaving, it can be replaced with: diff --git a/toolchains/sysimage/build_vfat_image.py b/toolchains/sysimage/build_vfat_image.py index 7bd1ec7f417b..c7f099e2a7e2 100755 --- a/toolchains/sysimage/build_vfat_image.py +++ b/toolchains/sysimage/build_vfat_image.py @@ -17,7 +17,15 @@ from toolchains.sysimage.utils import parse_size -def untar_to_vfat(tf, fs_basedir, out_file, path_transform): +def mtools_cmd(mtools, subcommand, *extra_args): + # mtools is a multi-call binary; invoke a subcommand (mmd, mcopy, ...) via + # `mtools -c `. Wrapped in faketime for deterministic timestamps; + # the binary path is made absolute so faketime (which execs it) treats it as + # a path rather than searching PATH. + return ["faketime", "-f", "1970-1-1 0:0:0", os.path.abspath(mtools), "-c", subcommand, *extra_args] + + +def untar_to_vfat(tf, fs_basedir, out_file, path_transform, mtools): """ Put contents of tarfile into vfat image. @@ -39,45 +47,25 @@ def untar_to_vfat(tf, fs_basedir, out_file, path_transform): if path == "": continue os.mkdir(os.path.join(fs_basedir, path)) - subprocess.run(["faketime", "-f", "1970-1-1 0:0:0", "mmd", "-i", out_file, "::/" + path], check=True) + subprocess.run(mtools_cmd(mtools, "mmd", "-i", out_file, "::/" + path), check=True) elif member.type == tarfile.REGTYPE or member.type == tarfile.AREGTYPE: with open(os.path.join(fs_basedir, path), "wb") as f: f.write(tf.extractfile(member).read()) subprocess.run( - [ - "faketime", - "-f", - "1970-1-1 0:0:0", - "mcopy", - "-o", - "-i", - out_file, - os.path.join(fs_basedir, path), - "::/" + path, - ], + mtools_cmd(mtools, "mcopy", "-o", "-i", out_file, os.path.join(fs_basedir, path), "::/" + path), check=True, ) else: raise RuntimeError("Unhandled tar member kind: %s" % member.type) -def install_extra_files(out_file, extra_files, path_transform): +def install_extra_files(out_file, extra_files, path_transform, mtools): for extra_file in extra_files: source_file, install_target, mode = extra_file.split(":") if install_target[0] == "/": install_target = install_target[1:] subprocess.run( - [ - "faketime", - "-f", - "1970-1-1 0:0:0", - "mcopy", - "-o", - "-i", - out_file, - source_file, - "::/" + path_transform(install_target), - ], + mtools_cmd(mtools, "mcopy", "-o", "-i", out_file, source_file, "::/" + path_transform(install_target)), check=True, ) @@ -106,6 +94,10 @@ def main(): ) parser.add_argument("--dflate", help="Path to our dflate tool", type=str) parser.add_argument("--zstd", help="Path to the zstd tool", type=str) + # mkfs.fat is the same binary as mkfs.vfat (upstream ships the latter as a + # symlink); behaviour does not depend on the name it is invoked under. + parser.add_argument("--mkfs-fat", help="Path to the mkfs.fat (mkfs.vfat) tool", type=str, required=True) + parser.add_argument("--mtools", help="Path to the mtools tool", type=str, required=True) args = parser.parse_args(sys.argv[1:]) @@ -130,13 +122,13 @@ def path_transform(path, limit_prefix=limit_prefix): os.close(os.open(image_file, os.O_CREAT | os.O_RDWR | os.O_CLOEXEC | os.O_EXCL, 0o600)) os.truncate(image_file, image_size) - subprocess.run(["/usr/sbin/mkfs.vfat", "-i", "0", image_file], check=True) + subprocess.run([os.path.abspath(args.mkfs_fat), "-i", "0", image_file], check=True) if in_file: with tarfile.open(in_file, mode="r|*") as tf: - untar_to_vfat(tf, fs_basedir, image_file, path_transform) + untar_to_vfat(tf, fs_basedir, image_file, path_transform, args.mtools) - install_extra_files(image_file, extra_files, path_transform) + install_extra_files(image_file, extra_files, path_transform, args.mtools) # We use our tool, dflate, to quickly create a sparse, deterministic, tar. # If dflate is ever misbehaving, it can be replaced with: diff --git a/toolchains/sysimage/toolchain.bzl b/toolchains/sysimage/toolchain.bzl index d3c8d7d9af2c..45c7071461b9 100644 --- a/toolchains/sysimage/toolchain.bzl +++ b/toolchains/sysimage/toolchain.bzl @@ -160,6 +160,15 @@ build_container_filesystem = _icos_build_rule( }, ) +# rules_foreign_cc exposes a tool's binaries (plus an include dir) under a single +# target; pick out the binary we need by name. Mirrors the lookup done for +# //:mkfs.ext4 in the ext4_image rule. +def _find_tool(files, basename): + for f in files: + if f.basename == basename: + return f + fail("could not locate '{}' binary among tool outputs".format(basename)) + def _vfat_image_impl(ctx): args = [] inputs = [] @@ -174,6 +183,10 @@ def _vfat_image_impl(ctx): args.extend(["-i", src_file.path]) inputs.extend(ctx.files.src) + mkfs_fat = _find_tool(ctx.files._dosfstools, "mkfs.fat") + mtools = _find_tool(ctx.files._mtools, "mtools") + inputs.extend(ctx.files._dosfstools + ctx.files._mtools) + args.extend([ "-s", ctx.attr.partition_size, @@ -183,6 +196,10 @@ def _vfat_image_impl(ctx): ctx.executable._dflate.path, "--zstd", ctx.executable._zstd.path, + "--mkfs-fat", + mkfs_fat.path, + "--mtools", + mtools.path, ]) for input_target, install_target in ctx.attr.extra_files.items(): @@ -230,6 +247,16 @@ vfat_image = _icos_build_rule( executable = True, cfg = "exec", ), + "_dosfstools": attr.label( + default = "//:mkfs.fat", + cfg = "exec", + allow_files = True, + ), + "_mtools": attr.label( + default = "//:mtools", + cfg = "exec", + allow_files = True, + ), }, ) @@ -247,6 +274,11 @@ def _fat32_image_impl(ctx): args.extend(["-i", src_file.path]) inputs.extend(ctx.files.src) + mkfs_fat = _find_tool(ctx.files._dosfstools, "mkfs.fat") + fatlabel = _find_tool(ctx.files._dosfstools, "fatlabel") + mtools = _find_tool(ctx.files._mtools, "mtools") + inputs.extend(ctx.files._dosfstools + ctx.files._mtools) + args.extend([ "-s", ctx.attr.partition_size, @@ -256,6 +288,12 @@ def _fat32_image_impl(ctx): ctx.executable._dflate.path, "--zstd", ctx.executable._zstd.path, + "--mkfs-fat", + mkfs_fat.path, + "--fatlabel", + fatlabel.path, + "--mtools", + mtools.path, ]) for input_target, install_target in ctx.attr.extra_files.items(): @@ -307,6 +345,16 @@ fat32_image = _icos_build_rule( executable = True, cfg = "exec", ), + "_dosfstools": attr.label( + default = "//:mkfs.fat", + cfg = "exec", + allow_files = True, + ), + "_mtools": attr.label( + default = "//:mtools", + cfg = "exec", + allow_files = True, + ), }, ) From a07490d46400132952f0c7029788400cdcfdbae4 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Thu, 11 Jun 2026 20:16:29 +0200 Subject: [PATCH 21/75] fix: bind danted to the wildcard address instead of an interface name (#10445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Root Cause On PRs where testnet allocation is pinned to the local DC (#10436), `//rs/tests/networking:canister_http_socks_test` fails with every outcall attempt erroring as: ``` direct connect ... ConnectionRefused and connect through socks "... Connector(ConnectError(\"tcp connect error\", ... ConnectionRefused))" ``` The *direct* refusal is intentional (the test injects an egress-reject rule for httpbin first). The real failure is the SOCKS leg: the nested `Connector(ConnectError(...))` means the TCP connection **to the SOCKS proxy itself** — API boundary node port 1080 — was refused, on every attempt for the entire run, on **both** API BNs. A "connection refused" (RST) pins this down precisely: - The API BN firewall uses `policy drop` with an explicit `accept` for node IPs on port 1080, so a firewall problem would cause *timeouts*, not RSTs. - An RST means the BN was up, its global IPv6 was configured in the kernel, the packet passed the firewall — and **nothing was listening on :1080**. `danted.conf` configured the listener as: ``` internal: enp1s0 port = 1080 ``` Dante resolves an interface name to its addresses **once at startup** and never re-binds. GuestOS receives its global IPv6 via SLAAC. If `danted.service` starts while `enp1s0` only has its link-local address (router advertisement not yet processed), danted binds the link-local scope only and the global `[...]:1080` endpoint stays closed forever — `Restart=always` never kicks in because danted keeps running happily. This is confirmed directly by the journald logs of one of the failing API BNs: ``` 13:57:58.543 enp1s0: Gained IPv6LL 13:57:58.547 Finished systemd-networkd-wait-online.service - Wait for Network to be Online. 13:57:58.549 Reached target network-online.target - Network is Online. 13:57:58.553 Started danted.service - SOCKS (v4 and v5) proxy daemon (danted). ... 13:57:58.637 danted[1030]: info: Dante/server[1/1] v1.4.4 running ``` `systemd-networkd-wait-online` completed 4 ms after the interface gained only its **link-local** address — `network-online.target` does not guarantee a global SLAAC address — and danted started 6 ms later, binding the link-local address only. This race got amplified by DC pinning: in the failing run all nine VMs of the testnet (5 replicas, 2 API BNs, 2 UVMs) were packed onto a single host, slowing down boot and RA/SLAAC delivery enough to hit the race on both API BNs at once. The bug is pre-existing on `master`; the DC pinning only widened the window. The same fragility was previously patched around in #4658 (`PartOf=systemd-networkd.service` to restart danted when networkd restarts). ## Fix Bind the wildcard address instead of an interface name: ``` internal: :: port = 1080 ``` A wildcard bind does not depend on address assignment timing — connections to the global address succeed as soon as the address exists. Access to the SOCKS proxy remains restricted through the firewall, which only whitelists node IPs on port 1080 (the config already noted "Allow everyone - this is already restricted through the firewall"). `external: enp1s0` (the outgoing side) is unchanged. ## Verification - `bazel test --runs_per_test=3 //rs/tests/networking:canister_http_socks_test` passes 3/3 (avg 186 s) on the DC-pinned branch that previously reproduced the failure. --- ic-os/components/networking/socks-proxy/danted.conf | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ic-os/components/networking/socks-proxy/danted.conf b/ic-os/components/networking/socks-proxy/danted.conf index dec406a04f2a..1eed0621484c 100755 --- a/ic-os/components/networking/socks-proxy/danted.conf +++ b/ic-os/components/networking/socks-proxy/danted.conf @@ -2,7 +2,14 @@ logoutput: stdout # Interfaces to use -internal: enp1s0 port = 1080 +# +# Listen on the wildcard address instead of an interface name: danted resolves +# an interface name to its addresses once at startup and never re-binds, so if +# it starts before SLAAC has assigned the global IPv6 address it would listen +# on the link-local address only, making connections to the global address fail +# with "connection refused" forever. A wildcard bind does not depend on address +# assignment timing. Access is restricted through the firewall. +internal: :: port = 1080 external: enp1s0 # Privileges From 188c834057cf4646640bc89f1c8468aff1613e60 Mon Sep 17 00:00:00 2001 From: Stefan Schneider <31004026+schneiderstefan@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:06:04 +0200 Subject: [PATCH 22/75] feat: Introduce EngineNotAllowed signals (#10432) This commit introduces XNet reject signals that will fire when messages are sent to/from engines that are not allowed. The current PR only introduces the ability to read these signals, without writing them themselves. The actual implementation of XNet with engines will come in a follow-up, and will start sending these signals. --- Cargo.lock | 1 + .../certification_version/src/lib.rs | 4 +- rs/canonical_state/src/encoding/old_types.rs | 217 +++++++++++++++++- .../src/encoding/tests/compatibility.rs | 128 +++++++++-- .../src/encoding/tests/conversion.rs | 1 + .../src/encoding/tests/test_fixtures.rs | 22 +- rs/canonical_state/src/encoding/types.rs | 15 +- rs/canonical_state/tests/compatibility.rs | 44 +++- .../tests/size_limit_visitor.rs | 2 +- rs/messaging/src/routing/stream_handler.rs | 4 + rs/protobuf/def/state/queues/v1/queues.proto | 1 + rs/protobuf/src/gen/state/state.queues.v1.rs | 3 + rs/protobuf/src/gen/types/state.queues.v1.rs | 3 + rs/replicated_state/BUILD.bazel | 1 + .../src/metadata_state/tests.rs | 2 +- rs/replicated_state/tests/metadata_state.rs | 5 +- rs/state_manager/src/stream_encoding/tests.rs | 2 + rs/state_manager/src/tree_hash.rs | 4 + rs/state_manager/tests/state_manager.rs | 13 ++ rs/test_utilities/src/state_manager.rs | 3 + rs/test_utilities/state/BUILD.bazel | 1 + rs/test_utilities/state/Cargo.toml | 1 + rs/test_utilities/state/src/lib.rs | 40 +++- rs/types/types/src/xnet.rs | 6 + .../tests/certified_slice_pool.rs | 16 +- .../tests/xnet_payload_builder.rs | 28 ++- 26 files changed, 521 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 324d36b00ebe..16bcb0f104fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15300,6 +15300,7 @@ version = "0.9.0" dependencies = [ "ic-base-types", "ic-btc-replica-types", + "ic-certification-version", "ic-interfaces", "ic-management-canister-types-private", "ic-registry-routing-table", diff --git a/rs/canonical_state/certification_version/src/lib.rs b/rs/canonical_state/certification_version/src/lib.rs index f3e3064ecb48..55a1c9c567c6 100644 --- a/rs/canonical_state/certification_version/src/lib.rs +++ b/rs/canonical_state/certification_version/src/lib.rs @@ -20,6 +20,8 @@ pub enum CertificationVersion { V24 = 24, /// Add /subnet//type. V25 = 25, + /// Introdue `EngineNotAllowed` signals + V26 = 26, } #[derive(Eq, PartialEq, Debug)] @@ -63,7 +65,7 @@ pub const MIN_SUPPORTED_CERTIFICATION_VERSION: CertificationVersion = Certificat /// /// The replica will panic if requested to certify using a version higher than /// this. -pub const MAX_SUPPORTED_CERTIFICATION_VERSION: CertificationVersion = CertificationVersion::V25; +pub const MAX_SUPPORTED_CERTIFICATION_VERSION: CertificationVersion = CertificationVersion::V26; /// Returns a list of all certification versions from `MIN_SUPPORTED_CERTIFICATION_VERSION` /// up to `MAX_SUPPORTED_CERTIFICATION_VERSION`. diff --git a/rs/canonical_state/src/encoding/old_types.rs b/rs/canonical_state/src/encoding/old_types.rs index e5adca129036..f84c5f4cd732 100644 --- a/rs/canonical_state/src/encoding/old_types.rs +++ b/rs/canonical_state/src/encoding/old_types.rs @@ -7,6 +7,7 @@ //! be removed. use std::{ + collections::{BTreeMap, HashMap, VecDeque}, convert::{TryFrom, TryInto}, sync::Arc, }; @@ -17,7 +18,11 @@ use crate::encoding::types::{ Bytes, Cycles, Funds, Payload, Refund, RejectSignals, STREAM_SUPPORTED_FLAGS, StreamFlagBits, }; use ic_protobuf::proxy::ProxyDecodeError; -use ic_types::{Time, time::CoarseTime}; +use ic_types::{ + Time, + time::CoarseTime, + xnet::{RejectReason, RejectSignal, StreamIndex}, +}; use serde::{Deserialize, Serialize}; /// Canonical representation of `ic_types::messages::RequestOrResponse` at certification version V19. @@ -513,3 +518,213 @@ impl TryFrom for ic_types::xnet::StreamHeader { )) } } + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct RejectSignalsV25 { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub canister_migrating_deltas: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub canister_not_found_deltas: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub canister_stopped_deltas: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub canister_stopping_deltas: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub queue_full_deltas: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub out_of_memory_deltas: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub unknown_deltas: Vec, +} + +impl RejectSignalsV25 { + pub fn is_empty(&self) -> bool { + let Self { + canister_migrating_deltas, + canister_not_found_deltas, + canister_stopped_deltas, + canister_stopping_deltas, + queue_full_deltas, + out_of_memory_deltas, + unknown_deltas, + } = self; + canister_migrating_deltas.is_empty() + && canister_not_found_deltas.is_empty() + && canister_stopped_deltas.is_empty() + && canister_stopping_deltas.is_empty() + && queue_full_deltas.is_empty() + && out_of_memory_deltas.is_empty() + && unknown_deltas.is_empty() + } +} + +impl From<(&VecDeque, StreamIndex, CertificationVersion)> for RejectSignalsV25 { + fn from( + (reject_signals, signals_end, _certification_version): ( + &VecDeque, + StreamIndex, + CertificationVersion, + ), + ) -> Self { + // Demux `reject_signals` into vectors of `StreamIndex`. + let mut demuxed = HashMap::>::new(); + for RejectSignal { reason, index } in reject_signals.iter() { + demuxed.entry(*reason).or_default().push(*index) + } + let mut deltas_for = |reason| -> Vec { + demuxed + .remove(&reason) + .map(|signals| { + let mut next_index = signals_end; + let mut reject_signal_deltas = vec![0; signals.len()]; + for (i, stream_index) in signals.iter().enumerate().rev() { + assert!(next_index > *stream_index); + reject_signal_deltas[i] = next_index.get() - stream_index.get(); + next_index = *stream_index; + } + reject_signal_deltas + }) + .unwrap_or_default() + }; + + assert!(deltas_for(RejectReason::EngineNotAllowed).is_empty()); + + RejectSignalsV25 { + canister_migrating_deltas: deltas_for(RejectReason::CanisterMigrating), + canister_not_found_deltas: deltas_for(RejectReason::CanisterNotFound), + canister_stopped_deltas: deltas_for(RejectReason::CanisterStopped), + canister_stopping_deltas: deltas_for(RejectReason::CanisterStopping), + queue_full_deltas: deltas_for(RejectReason::QueueFull), + out_of_memory_deltas: deltas_for(RejectReason::OutOfMemory), + unknown_deltas: deltas_for(RejectReason::Unknown), + } + } +} + +fn try_from_deltas_v25( + reject_signals: &RejectSignalsV25, + signals_end: u64, +) -> Result, ProxyDecodeError> { + use RejectReason::*; + + let mut reject_signals_map = BTreeMap::::new(); + for (reason, deltas) in [ + (CanisterMigrating, &reject_signals.canister_migrating_deltas), + (CanisterNotFound, &reject_signals.canister_not_found_deltas), + (CanisterStopped, &reject_signals.canister_stopped_deltas), + (CanisterStopping, &reject_signals.canister_stopping_deltas), + (QueueFull, &reject_signals.queue_full_deltas), + (OutOfMemory, &reject_signals.out_of_memory_deltas), + (Unknown, &reject_signals.unknown_deltas), + ] { + let mut stream_index = StreamIndex::new(signals_end); + for delta in deltas.iter().rev() { + if *delta == 0 { + // Reject signal deltas are invalid; a delta of `0` is forbidden since it would + // lead to duplicates or a stream_index of `signals_end`. + return Err(ProxyDecodeError::Other(format!( + "StreamHeader: {reason:?} found bad delta: `0` is not allowed in `reject_signal_deltas` {deltas:?}", + ))); + } + if stream_index < StreamIndex::new(*delta) { + // Reject signal deltas are invalid. + return Err(ProxyDecodeError::Other(format!( + "StreamHeader: {reason:?} reject signals are invalid, got `signals_end` {signals_end:?}, `reject_signal_deltas` {deltas:?}", + ))); + } + stream_index -= StreamIndex::new(*delta); + + if reject_signals_map.insert(stream_index, reason).is_some() { + return Err(ProxyDecodeError::Other( + "StreamHeader: reject signals are invalid, got duplicates".to_string(), + )); + } + } + } + + Ok(reject_signals_map + .iter() + .map(|(index, reason)| RejectSignal::new(*reason, *index)) + .collect()) +} + +/// Canonical representation of `ic_types::xnet::StreamHeader` at certification version V25. +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct StreamHeaderV25 { + pub begin: u64, + pub end: u64, + pub signals_end: u64, + #[serde(default, skip_serializing_if = "types::is_zero")] + pub reserved_3: u64, + #[serde(default, skip_serializing_if = "types::is_zero")] + pub flags: u64, + #[serde(default, skip_serializing_if = "RejectSignalsV25::is_empty")] + pub reject_signals: RejectSignalsV25, +} + +impl From<(&ic_types::xnet::StreamHeader, CertificationVersion)> for StreamHeaderV25 { + fn from( + (header, certification_version): (&ic_types::xnet::StreamHeader, CertificationVersion), + ) -> Self { + let mut flags = 0; + let ic_types::xnet::StreamFlags { + deprecated_responses_only, + } = *header.flags(); + if deprecated_responses_only { + flags |= StreamFlagBits::DeprecatedResponsesOnly as u64; + } + + // Generate deltas representation based on `certification_version` to ensure unique + // encoding. + let reject_signals = ( + header.reject_signals(), + header.signals_end(), + certification_version, + ) + .into(); + + Self { + begin: header.begin().get(), + end: header.end().get(), + signals_end: header.signals_end().get(), + reserved_3: 0, + flags, + reject_signals, + } + } +} + +impl TryFrom for ic_types::xnet::StreamHeader { + type Error = ProxyDecodeError; + fn try_from(header: StreamHeaderV25) -> Result { + if header.reserved_3 != 0 { + return Err(ProxyDecodeError::Other(format!( + "StreamHeader: field index 3 is populated: {:?}", + header.reserved_3, + ))); + } + if header.flags & !STREAM_SUPPORTED_FLAGS != 0 { + return Err(ProxyDecodeError::Other(format!( + "StreamHeader: unsupported flags: got `flags` {:#b}, `supported_flags` {:#b}", + header.flags, STREAM_SUPPORTED_FLAGS, + ))); + } + let flags = ic_types::xnet::StreamFlags { + deprecated_responses_only: header.flags + & StreamFlagBits::DeprecatedResponsesOnly as u64 + != 0, + }; + + let reject_signals = try_from_deltas_v25(&header.reject_signals, header.signals_end)?; + + Ok(Self::new( + header.begin.into(), + header.end.into(), + header.signals_end.into(), + reject_signals, + flags, + )) + } +} diff --git a/rs/canonical_state/src/encoding/tests/compatibility.rs b/rs/canonical_state/src/encoding/tests/compatibility.rs index 6cb294a7ef90..6b3a0e6411a8 100644 --- a/rs/canonical_state/src/encoding/tests/compatibility.rs +++ b/rs/canonical_state/src/encoding/tests/compatibility.rs @@ -166,6 +166,102 @@ fn canonical_encoding_stream_header() { } } +/// Canonical CBOR encoding (versions `V26` and above) of: +/// +/// ```no_run +/// StreamHeader { +/// begin: 23.into(), +/// end: 25.into(), +/// signals_end: 256.into(), +/// reject_signals: vec![ +/// RejectSignal::new(RejectReason::CanisterMigrating, 249.into()), +/// RejectSignal::new(RejectReason::CanisterNotFound, 250.into()), +/// RejectSignal::new(RejectReason::QueueFull, 251.into()), +/// RejectSignal::new(RejectReason::OutOfMemory, 252.into()), +/// RejectSignal::new(RejectReason::CanisterStopping, 253.into()), +/// RejectSignal::new(RejectReason::CanisterStopped, 254.into()), +/// RejectSignal::new(RejectReason::Unknown, 255.into()), +/// RejectSignal::new(RejectReason::EngineNotAllowed, 248.into()), +/// ] +/// .into(), +/// flags: StreamFlags { +/// deprecated_responses_only: true, +/// }, +/// } +/// ``` +/// +/// Expected: +/// +/// ```text +/// A5 # map(5) +/// 00 # field_index(StreamHeader::begin) +/// 17 # unsigned(23) +/// 01 # field_index(StreamHeader::end) +/// 18 19 # unsigned(25) +/// 02 # field_index(StreamHeader::signals_end) +/// 19 0100 # unsigned(256) +/// 04 # field_index(StreamHeader::flags) +/// 01 # unsigned(1) +/// 05 # field_index(StreamHeader::reject_signals) +/// A8 # map(8) +/// 00 # field_index(RejectSignals::canister_migrating_deltas) +/// 81 # array(1) +/// 07 # unsigned(7) +/// 01 # field_index(RejectSignals::canister_not_found_deltas) +/// 81 # array(1) +/// 06 # unsigned(6) +/// 02 # field_index(RejectSignals::canister_stopped_deltas) +/// 81 # array(1) +/// 02 # unsigned(2) +/// 03 # field_index(RejectSignals::canister_stopping_deltas) +/// 81 # array(1) +/// 03 # unsigned(3) +/// 04 # field_index(RejectSignals::queue_full_deltas) +/// 81 # array(1) +/// 05 # unsigned(5) +/// 05 # field_index(RejectSignals::out_of_memory_deltas) +/// 81 # array(1) +/// 04 # unsigned(4) +/// 06 # field_index(RejectSignals::unknown_deltas) +/// 81 # array(1) +/// 01 # unsigned(1) +/// 07 # field_index(RejectSignals::engine_not_allowed_deltas) +/// 81 # array(1) +/// 08 # unsigned(8) +/// ``` +/// Used http://cbor.me/ for printing the human friendly output. +#[test] +fn canonical_encoding_stream_header_v26() { + for certification_version in + all_supported_versions().filter(|v| *v >= CertificationVersion::V26) + { + let header = StreamHeader::new( + 23.into(), + 25.into(), + 256.into(), + vec![ + RejectSignal::new(RejectReason::CanisterMigrating, 249.into()), + RejectSignal::new(RejectReason::CanisterNotFound, 250.into()), + RejectSignal::new(RejectReason::QueueFull, 251.into()), + RejectSignal::new(RejectReason::OutOfMemory, 252.into()), + RejectSignal::new(RejectReason::CanisterStopping, 253.into()), + RejectSignal::new(RejectReason::CanisterStopped, 254.into()), + RejectSignal::new(RejectReason::Unknown, 255.into()), + RejectSignal::new(RejectReason::EngineNotAllowed, 248.into()), + ] + .into(), + StreamFlags { + deprecated_responses_only: true, + }, + ); + + assert_eq!( + "A5 00 17 01 18 19 02 19 01 00 04 01 05 A8 00 81 07 01 81 06 02 81 02 03 81 03 04 81 05 05 81 04 06 81 01 07 81 08", + as_hex(&encode_stream_header(&header, certification_version)) + ); + } +} + /// Canonical CBOR encoding of: /// /// ```no_run @@ -1910,21 +2006,25 @@ fn stream_header(certification_version: CertificationVersion) -> StreamHeader { } fn reject_signals( - _certification_version: CertificationVersion, + certification_version: CertificationVersion, ) -> (VecDeque, StreamIndex) { - ( - vec![ - RejectSignal::new(RejectReason::CanisterMigrating, 249.into()), - RejectSignal::new(RejectReason::CanisterNotFound, 250.into()), - RejectSignal::new(RejectReason::CanisterStopped, 251.into()), - RejectSignal::new(RejectReason::CanisterStopping, 252.into()), - RejectSignal::new(RejectReason::QueueFull, 253.into()), - RejectSignal::new(RejectReason::OutOfMemory, 254.into()), - RejectSignal::new(RejectReason::Unknown, 255.into()), - ] - .into(), - 256.into(), - ) + let mut reject_signals = vec![ + RejectSignal::new(RejectReason::CanisterMigrating, 249.into()), + RejectSignal::new(RejectReason::CanisterNotFound, 250.into()), + RejectSignal::new(RejectReason::CanisterStopped, 251.into()), + RejectSignal::new(RejectReason::CanisterStopping, 252.into()), + RejectSignal::new(RejectReason::QueueFull, 253.into()), + RejectSignal::new(RejectReason::OutOfMemory, 254.into()), + RejectSignal::new(RejectReason::Unknown, 255.into()), + ]; + // `EngineNotAllowed` signals are only encodable starting at V26. + if certification_version >= CertificationVersion::V26 { + reject_signals.push(RejectSignal::new( + RejectReason::EngineNotAllowed, + 248.into(), + )); + } + (reject_signals.into(), 256.into()) } fn request_message(certification_version: CertificationVersion) -> StreamMessage { diff --git a/rs/canonical_state/src/encoding/tests/conversion.rs b/rs/canonical_state/src/encoding/tests/conversion.rs index 141b2f074010..c491f2d664e1 100644 --- a/rs/canonical_state/src/encoding/tests/conversion.rs +++ b/rs/canonical_state/src/encoding/tests/conversion.rs @@ -83,6 +83,7 @@ fn with_stream_header_deltas( QueueFull => header.reject_signals.queue_full_deltas = deltas, OutOfMemory => header.reject_signals.out_of_memory_deltas = deltas, Unknown => header.reject_signals.unknown_deltas = deltas, + EngineNotAllowed => header.reject_signals.engine_not_allowed_deltas = deltas, } header } diff --git a/rs/canonical_state/src/encoding/tests/test_fixtures.rs b/rs/canonical_state/src/encoding/tests/test_fixtures.rs index a49005919e2a..fa24fe6d11e3 100644 --- a/rs/canonical_state/src/encoding/tests/test_fixtures.rs +++ b/rs/canonical_state/src/encoding/tests/test_fixtures.rs @@ -12,8 +12,8 @@ use ic_types::{ }; use ic_types_cycles::Cycles; -pub fn stream_header(_certification_version: CertificationVersion) -> StreamHeader { - let reject_signals = vec![ +pub fn stream_header(certification_version: CertificationVersion) -> StreamHeader { + let mut reject_signals = vec![ RejectSignal::new(RejectReason::CanisterMigrating, 10.into()), RejectSignal::new(RejectReason::CanisterNotFound, 200.into()), RejectSignal::new(RejectReason::OutOfMemory, 250.into()), @@ -21,13 +21,25 @@ pub fn stream_header(_certification_version: CertificationVersion) -> StreamHead RejectSignal::new(RejectReason::CanisterStopped, 252.into()), RejectSignal::new(RejectReason::QueueFull, 253.into()), RejectSignal::new(RejectReason::Unknown, 254.into()), - ] - .into(); + ]; + // `EngineNotAllowed` signals are only encodable starting at V26. + if certification_version >= CertificationVersion::V26 { + reject_signals.push(RejectSignal::new( + RejectReason::EngineNotAllowed, + 255.into(), + )); + } let flags = StreamFlags { deprecated_responses_only: true, }; - StreamHeader::new(23.into(), 25.into(), 256.into(), reject_signals, flags) + StreamHeader::new( + 23.into(), + 25.into(), + 256.into(), + reject_signals.into(), + flags, + ) } pub fn request(_certification_version: CertificationVersion) -> StreamMessage { diff --git a/rs/canonical_state/src/encoding/types.rs b/rs/canonical_state/src/encoding/types.rs index fa1c0c81e32c..fc68f0608e11 100644 --- a/rs/canonical_state/src/encoding/types.rs +++ b/rs/canonical_state/src/encoding/types.rs @@ -67,6 +67,8 @@ pub struct RejectSignals { pub out_of_memory_deltas: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub unknown_deltas: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub engine_not_allowed_deltas: Vec, } impl RejectSignals { @@ -78,6 +80,7 @@ impl RejectSignals { && self.queue_full_deltas.is_empty() && self.out_of_memory_deltas.is_empty() && self.unknown_deltas.is_empty() + && self.engine_not_allowed_deltas.is_empty() } } @@ -297,7 +300,7 @@ impl TryFrom for ic_types::xnet::StreamHeader { impl From<(&VecDeque, StreamIndex, CertificationVersion)> for RejectSignals { fn from( - (reject_signals, signals_end, _certification_version): ( + (reject_signals, signals_end, certification_version): ( &VecDeque, StreamIndex, CertificationVersion, @@ -324,6 +327,14 @@ impl From<(&VecDeque, StreamIndex, CertificationVersion)> for Reje .unwrap_or_default() }; + let engine_not_allowed_deltas = deltas_for(RejectReason::EngineNotAllowed); + assert!( + certification_version >= CertificationVersion::V26 + || engine_not_allowed_deltas.is_empty(), + "`EngineNotAllowed` reject signals must not be encoded before certification version V26, \ + got certification version {certification_version:?}", + ); + RejectSignals { canister_migrating_deltas: deltas_for(RejectReason::CanisterMigrating), canister_not_found_deltas: deltas_for(RejectReason::CanisterNotFound), @@ -332,6 +343,7 @@ impl From<(&VecDeque, StreamIndex, CertificationVersion)> for Reje queue_full_deltas: deltas_for(RejectReason::QueueFull), out_of_memory_deltas: deltas_for(RejectReason::OutOfMemory), unknown_deltas: deltas_for(RejectReason::Unknown), + engine_not_allowed_deltas, } } } @@ -351,6 +363,7 @@ pub(crate) fn try_from_deltas( (QueueFull, &reject_signals.queue_full_deltas), (OutOfMemory, &reject_signals.out_of_memory_deltas), (Unknown, &reject_signals.unknown_deltas), + (EngineNotAllowed, &reject_signals.engine_not_allowed_deltas), ] { let mut stream_index = StreamIndex::new(signals_end); for delta in deltas.iter().rev() { diff --git a/rs/canonical_state/tests/compatibility.rs b/rs/canonical_state/tests/compatibility.rs index 62c46a8815f7..ba041bf19231 100644 --- a/rs/canonical_state/tests/compatibility.rs +++ b/rs/canonical_state/tests/compatibility.rs @@ -3,9 +3,9 @@ use ic_canonical_state::{ CertificationVersion, MAX_SUPPORTED_CERTIFICATION_VERSION, MIN_SUPPORTED_CERTIFICATION_VERSION, encoding::{ CborProxyDecoder, CborProxyEncoder, - old_types::{RequestOrResponseV21, StreamHeaderV19, StreamMessageV22}, + old_types::{RequestOrResponseV21, StreamHeaderV19, StreamHeaderV25, StreamMessageV22}, types::{ - StreamHeader as StreamHeaderV21, StreamMessage as StreamMessageV23, + StreamHeader as StreamHeaderV26, StreamMessage as StreamMessageV23, SubnetMetrics as SubnetMetricsV21, SystemMetadata as SystemMetadataV21, }, }, @@ -72,14 +72,27 @@ pub(crate) fn arb_valid_versioned_stream_header( ) -> impl Strategy)> { prop_oneof![ // Stream headers may have flavours of reject signals other than `CanisterMigrating` - // starting from certification version 19. + // starting from certification version 19, except `EngineNotAllowed` (valid from V26). ( arb_stream_header( /* min_signal_count */ 0, max_signal_count, - /* with_reject_reasons */ RejectReason::all(), + /* with_reject_reasons */ + RejectReason::all() + .into_iter() + .filter(|reason| *reason != RejectReason::EngineNotAllowed) + .collect(), ), Just(CertificationVersion::V19..=MAX_SUPPORTED_CERTIFICATION_VERSION) + ), + // `EngineNotAllowed` reject signals are valid from certification version 26. + ( + arb_stream_header( + /* min_signal_count */ 0, + max_signal_count, + /* with_reject_reasons */ RejectReason::all(), + ), + Just(CertificationVersion::V26..=MAX_SUPPORTED_CERTIFICATION_VERSION) ) ] } @@ -90,9 +103,18 @@ pub(crate) fn arb_invalid_versioned_stream_header( ) -> impl Strategy)> { prop_oneof![ // Encoding a stream header with reject signal flavors other than `CanisterMigrating` - // before certification version 19 should panic. + // before certification version 19 should panic. `EngineNotAllowed` is excluded as it is + // only encodable from V26. ( - arb_invalid_stream_header(/* min_signal_count */ 1, max_signal_count), + arb_invalid_stream_header( + /* min_signal_count */ 1, + max_signal_count, + /* with_reject_reasons */ + RejectReason::all() + .into_iter() + .filter(|reason| *reason != RejectReason::EngineNotAllowed) + .collect(), + ), Just(CertificationVersion::V19..=MAX_SUPPORTED_CERTIFICATION_VERSION), ), ] @@ -109,11 +131,17 @@ lazy_static! { |v| StreamHeaderV19::proxy_encode(v), |v| StreamHeaderV19::proxy_decode(v), ), + VersionedEncoding::new( + MIN_SUPPORTED_CERTIFICATION_VERSION..=CertificationVersion::V25, + "StreamHeaderV25", + |v| StreamHeaderV25::proxy_encode(v), + |v| StreamHeaderV25::proxy_decode(v), + ), VersionedEncoding::new( MIN_SUPPORTED_CERTIFICATION_VERSION..=MAX_SUPPORTED_CERTIFICATION_VERSION, "StreamHeader", - |v| StreamHeaderV21::proxy_encode(v), - |v| StreamHeaderV21::proxy_decode(v), + |v| StreamHeaderV26::proxy_encode(v), + |v| StreamHeaderV26::proxy_decode(v), ), ]; } diff --git a/rs/canonical_state/tests/size_limit_visitor.rs b/rs/canonical_state/tests/size_limit_visitor.rs index ac66e3ec38c4..5eb7e34f37e0 100644 --- a/rs/canonical_state/tests/size_limit_visitor.rs +++ b/rs/canonical_state/tests/size_limit_visitor.rs @@ -29,7 +29,7 @@ struct Fixture { prop_compose! { /// An arbitrary fixture with default `slice_begin` and `size_limit` values. fn arb_barebone_fixture(max_size: usize) - (stream in arb_stream(0, max_size, 0, max_size)) -> Fixture { + (stream in arb_stream(0, max_size, 0, max_size, MAX_SUPPORTED_CERTIFICATION_VERSION)) -> Fixture { let begin = stream.messages_begin().get(); let end = stream.messages_end().get(); diff --git a/rs/messaging/src/routing/stream_handler.rs b/rs/messaging/src/routing/stream_handler.rs index 6a5e5193d782..4ba6ba7a3ac9 100644 --- a/rs/messaging/src/routing/stream_handler.rs +++ b/rs/messaging/src/routing/stream_handler.rs @@ -1210,6 +1210,10 @@ fn generate_reject_response_for(reason: RejectReason, request: &Request) -> Requ RejectCode::SysFatal, "Inducting request failed due to an unknown error".to_string(), ), + RejectReason::EngineNotAllowed => ( + RejectCode::SysFatal, + "Guaranteed-response calls and cycles transfers to/from CloudEngine subnets are not allowed".to_string(), + ), }; generate_reject_response(request, code, message) } diff --git a/rs/protobuf/def/state/queues/v1/queues.proto b/rs/protobuf/def/state/queues/v1/queues.proto index c870b1333a54..2454f2ecabf3 100644 --- a/rs/protobuf/def/state/queues/v1/queues.proto +++ b/rs/protobuf/def/state/queues/v1/queues.proto @@ -32,6 +32,7 @@ enum RejectReason { REJECT_REASON_QUEUE_FULL = 5; REJECT_REASON_OUT_OF_MEMORY = 6; REJECT_REASON_UNKNOWN = 7; + REJECT_REASON_ENGINE_NOT_ALLOWED = 8; } message RejectSignal { diff --git a/rs/protobuf/src/gen/state/state.queues.v1.rs b/rs/protobuf/src/gen/state/state.queues.v1.rs index cfd87b780904..c1797fc65b8d 100644 --- a/rs/protobuf/src/gen/state/state.queues.v1.rs +++ b/rs/protobuf/src/gen/state/state.queues.v1.rs @@ -300,6 +300,7 @@ pub enum RejectReason { QueueFull = 5, OutOfMemory = 6, Unknown = 7, + EngineNotAllowed = 8, } impl RejectReason { /// String value of the enum field names used in the ProtoBuf definition. @@ -316,6 +317,7 @@ impl RejectReason { Self::QueueFull => "REJECT_REASON_QUEUE_FULL", Self::OutOfMemory => "REJECT_REASON_OUT_OF_MEMORY", Self::Unknown => "REJECT_REASON_UNKNOWN", + Self::EngineNotAllowed => "REJECT_REASON_ENGINE_NOT_ALLOWED", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -329,6 +331,7 @@ impl RejectReason { "REJECT_REASON_QUEUE_FULL" => Some(Self::QueueFull), "REJECT_REASON_OUT_OF_MEMORY" => Some(Self::OutOfMemory), "REJECT_REASON_UNKNOWN" => Some(Self::Unknown), + "REJECT_REASON_ENGINE_NOT_ALLOWED" => Some(Self::EngineNotAllowed), _ => None, } } diff --git a/rs/protobuf/src/gen/types/state.queues.v1.rs b/rs/protobuf/src/gen/types/state.queues.v1.rs index 08001487edfe..dab2108a24e5 100644 --- a/rs/protobuf/src/gen/types/state.queues.v1.rs +++ b/rs/protobuf/src/gen/types/state.queues.v1.rs @@ -300,6 +300,7 @@ pub enum RejectReason { QueueFull = 5, OutOfMemory = 6, Unknown = 7, + EngineNotAllowed = 8, } impl RejectReason { /// String value of the enum field names used in the ProtoBuf definition. @@ -316,6 +317,7 @@ impl RejectReason { Self::QueueFull => "REJECT_REASON_QUEUE_FULL", Self::OutOfMemory => "REJECT_REASON_OUT_OF_MEMORY", Self::Unknown => "REJECT_REASON_UNKNOWN", + Self::EngineNotAllowed => "REJECT_REASON_ENGINE_NOT_ALLOWED", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -329,6 +331,7 @@ impl RejectReason { "REJECT_REASON_QUEUE_FULL" => Some(Self::QueueFull), "REJECT_REASON_OUT_OF_MEMORY" => Some(Self::OutOfMemory), "REJECT_REASON_UNKNOWN" => Some(Self::Unknown), + "REJECT_REASON_ENGINE_NOT_ALLOWED" => Some(Self::EngineNotAllowed), _ => None, } } diff --git a/rs/replicated_state/BUILD.bazel b/rs/replicated_state/BUILD.bazel index 0ffcd2e07c75..c77c7f9d75bd 100644 --- a/rs/replicated_state/BUILD.bazel +++ b/rs/replicated_state/BUILD.bazel @@ -158,6 +158,7 @@ rust_test_suite( ":replicated_state", "//packages/ic-error-types", "//rs/bitcoin/replica_types", + "//rs/canonical_state/certification_version", "//rs/protobuf", "//rs/registry/routing_table", "//rs/registry/subnet_type", diff --git a/rs/replicated_state/src/metadata_state/tests.rs b/rs/replicated_state/src/metadata_state/tests.rs index 35a403c0d15d..6407dbca03e1 100644 --- a/rs/replicated_state/src/metadata_state/tests.rs +++ b/rs/replicated_state/src/metadata_state/tests.rs @@ -2287,7 +2287,7 @@ fn compatibility_for_reject_reason() { RejectReason::iter() .map(|reason| reason as i32) .collect::>(), - [1, 2, 3, 4, 5, 6, 7] + [1, 2, 3, 4, 5, 6, 7, 8] ); } diff --git a/rs/replicated_state/tests/metadata_state.rs b/rs/replicated_state/tests/metadata_state.rs index 134730a70add..247d6147d650 100644 --- a/rs/replicated_state/tests/metadata_state.rs +++ b/rs/replicated_state/tests/metadata_state.rs @@ -1,10 +1,13 @@ +use ic_certification_version::MAX_SUPPORTED_CERTIFICATION_VERSION; use ic_protobuf::state::{queues::v1 as pb_queues, system_metadata::v1 as pb_metadata}; use ic_replicated_state::{Stream, metadata_state::SubnetMetrics}; use ic_test_utilities_state::{arb_stream, arb_subnet_metrics}; use std::convert::TryInto; #[test_strategy::proptest] -fn roundtrip_conversion_stream_proptest(#[strategy(arb_stream(0, 10, 0, 100))] stream: Stream) { +fn roundtrip_conversion_stream_proptest( + #[strategy(arb_stream(0, 10, 0, 100, MAX_SUPPORTED_CERTIFICATION_VERSION))] stream: Stream, +) { assert_eq!(stream, pb_queues::Stream::from(&stream).try_into().unwrap()); } diff --git a/rs/state_manager/src/stream_encoding/tests.rs b/rs/state_manager/src/stream_encoding/tests.rs index 16e13f3aad17..5f3c92086ee7 100644 --- a/rs/state_manager/src/stream_encoding/tests.rs +++ b/rs/state_manager/src/stream_encoding/tests.rs @@ -17,6 +17,7 @@ fn stream_encode_decode_roundtrip( 10, // max_size 0, // min_signal_count 10, // max_signal_count + MAX_SUPPORTED_CERTIFICATION_VERSION, ))] stream: Stream, ) { @@ -61,6 +62,7 @@ fn stream_encode_with_size_limit( 10, // max_size 0, // min_signal_count 10, // max_signal_count + MAX_SUPPORTED_CERTIFICATION_VERSION, ))] stream: Stream, #[strategy(0..1000_usize)] size_limit: usize, diff --git a/rs/state_manager/src/tree_hash.rs b/rs/state_manager/src/tree_hash.rs index f0b00b077c18..3881e9c47da7 100644 --- a/rs/state_manager/src/tree_hash.rs +++ b/rs/state_manager/src/tree_hash.rs @@ -245,6 +245,9 @@ mod tests { stream.push_reject_signal(RejectReason::OutOfMemory); stream.push_reject_signal(RejectReason::Unknown); stream.push_reject_signal(RejectReason::CanisterStopping); + if certification_version >= CertificationVersion::V26 { + stream.push_reject_signal(RejectReason::EngineNotAllowed); + } let loopback_stream = Stream::new( StreamIndexedQueue::with_begin(StreamIndex::from(13)), @@ -392,6 +395,7 @@ mod tests { "07797459A2F82D6F64628C0668C5BDB7F83447680DDB178208A40C2256409E8D", "F80B2659485C03F68935F214E4CB5D8CCAC02913DCA88E913C4B497F2120DA50", "416172D9AFD573236F1CDE2459756736EEB25028D64FB8D7192AAF33AFC0DA6F", + "057FA1842C06C958F79C6394C54E12F9C9DCF5036D186EBBB9A49CDB4E3683BF", ]; assert_eq!(expected_hashes.len(), all_supported_versions().count()); diff --git a/rs/state_manager/tests/state_manager.rs b/rs/state_manager/tests/state_manager.rs index cf1371514b3d..14c82f7ebea6 100644 --- a/rs/state_manager/tests/state_manager.rs +++ b/rs/state_manager/tests/state_manager.rs @@ -1,5 +1,6 @@ use assert_matches::assert_matches; use ic_base_types::SnapshotId; +use ic_canonical_state::CURRENT_CERTIFICATION_VERSION; use ic_canonical_state::encoding::encode_subnet_canister_ranges; use ic_canonical_state::lazy_tree_conversion::state_height_as_tree; use ic_canonical_state_tree_hash::lazy_tree::materialize::materialize; @@ -8325,6 +8326,7 @@ fn stream_store_encode_decode( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, #[strategy(0..20_usize)] size_limit: usize, @@ -8354,6 +8356,7 @@ fn stream_store_decode_with_modified_hash_fails( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, #[strategy(0..20_usize)] size_limit: usize, @@ -8386,6 +8389,7 @@ fn stream_store_decode_with_empty_witness_fails( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, #[strategy(0..20_usize)] size_limit: usize, @@ -8415,6 +8419,7 @@ fn stream_store_decode_slice_push_additional_message( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, ) { @@ -8467,6 +8472,7 @@ fn stream_store_decode_slice_modify_message_begin( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, ) { @@ -8506,6 +8512,7 @@ fn stream_store_decode_slice_modify_signals_end( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, ) { @@ -8542,6 +8549,7 @@ fn stream_store_decode_slice_push_signal( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, ) { @@ -8580,6 +8588,7 @@ fn stream_store_decode_with_invalid_destination( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, #[strategy(0..20_usize)] size_limit: usize, @@ -8610,6 +8619,7 @@ fn stream_store_decode_with_rejecting_verifier( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, #[strategy(0..20_usize)] size_limit: usize, @@ -8642,6 +8652,7 @@ fn stream_store_decode_with_invalid_destination_and_rejecting_verifier( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] stream: Stream, #[strategy(0..20_usize)] size_limit: usize, @@ -8671,6 +8682,7 @@ fn stream_store_encode_partial( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), #[strategy(0..1000_usize)] byte_limit: usize, @@ -8689,6 +8701,7 @@ fn stream_store_encode_partial_bad_indices( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), #[strategy(0..1000_usize)] byte_limit: usize, diff --git a/rs/test_utilities/src/state_manager.rs b/rs/test_utilities/src/state_manager.rs index db5eb0f6badc..f266bb2be77b 100644 --- a/rs/test_utilities/src/state_manager.rs +++ b/rs/test_utilities/src/state_manager.rs @@ -469,6 +469,7 @@ pub enum SerializableRejectReason { QueueFull = 5, OutOfMemory = 6, Unknown = 7, + EngineNotAllowed = 8, } impl From<&RejectReason> for SerializableRejectReason { @@ -481,6 +482,7 @@ impl From<&RejectReason> for SerializableRejectReason { RejectReason::QueueFull => Self::QueueFull, RejectReason::OutOfMemory => Self::OutOfMemory, RejectReason::Unknown => Self::Unknown, + RejectReason::EngineNotAllowed => Self::EngineNotAllowed, } } } @@ -495,6 +497,7 @@ impl From for RejectReason { SerializableRejectReason::QueueFull => RejectReason::QueueFull, SerializableRejectReason::OutOfMemory => RejectReason::OutOfMemory, SerializableRejectReason::Unknown => RejectReason::Unknown, + SerializableRejectReason::EngineNotAllowed => RejectReason::EngineNotAllowed, } } } diff --git a/rs/test_utilities/state/BUILD.bazel b/rs/test_utilities/state/BUILD.bazel index 0e4494e79fd6..6e7648d0136e 100644 --- a/rs/test_utilities/state/BUILD.bazel +++ b/rs/test_utilities/state/BUILD.bazel @@ -10,6 +10,7 @@ rust_library( deps = [ # Keep sorted. "//rs/bitcoin/replica_types", + "//rs/canonical_state/certification_version", "//rs/interfaces", "//rs/registry/routing_table", "//rs/registry/subnet_features", diff --git a/rs/test_utilities/state/Cargo.toml b/rs/test_utilities/state/Cargo.toml index 26ddb79bb796..84e79c7caa98 100644 --- a/rs/test_utilities/state/Cargo.toml +++ b/rs/test_utilities/state/Cargo.toml @@ -9,6 +9,7 @@ documentation.workspace = true [dependencies] ic-base-types = { path = "../../types/base_types" } ic-btc-replica-types = { path = "../../bitcoin/replica_types" } +ic-certification-version = { path = "../../canonical_state/certification_version" } ic-interfaces = { path = "../../interfaces" } ic-management-canister-types-private = { path = "../../types/management_canister_types" } ic-registry-routing-table = { path = "../../registry/routing_table" } diff --git a/rs/test_utilities/state/src/lib.rs b/rs/test_utilities/state/src/lib.rs index 8bdf4a6c3376..68fa55ddfb93 100644 --- a/rs/test_utilities/state/src/lib.rs +++ b/rs/test_utilities/state/src/lib.rs @@ -1,5 +1,6 @@ use ic_base_types::{EnvironmentVariables, NumSeconds}; use ic_btc_replica_types::BitcoinAdapterRequestWrapper; +use ic_certification_version::CertificationVersion; use ic_management_canister_types_private::{ CanisterStatusType, EcdsaCurve, EcdsaKeyId, LogVisibilityV2, MasterPublicKeyId, OnLowWasmMemoryHookStatus, SchnorrAlgorithm, SchnorrKeyId, @@ -869,6 +870,20 @@ pub fn insert_dummy_canister( state.put_canister_state(canister_state); } +/// Reject reasons encodable at `certification_version`. `EngineNotAllowed` is only encodable +/// from V26. +pub fn reject_reasons_encodable_at( + certification_version: CertificationVersion, +) -> Vec { + RejectReason::all() + .into_iter() + .filter(|reason| { + *reason != RejectReason::EngineNotAllowed + || certification_version >= CertificationVersion::V26 + }) + .collect() +} + prop_compose! { /// Produces a strategy that generates arbitrary stream signals. /// @@ -945,13 +960,19 @@ prop_compose! { /// Produces a strategy that generates a stream with between /// `[min_size, max_size]` messages and between /// `[min_signal_count, max_signal_count]` reject signals. - pub fn arb_stream(min_size: usize, max_size: usize, min_signal_count: usize, max_signal_count: usize)( + pub fn arb_stream( + min_size: usize, + max_size: usize, + min_signal_count: usize, + max_signal_count: usize, + certification_version: CertificationVersion, + )( stream in arb_stream_with_config( 0..=10000, min_size..=max_size, 0..=10000, min_signal_count..=max_signal_count, - RejectReason::all(), + reject_reasons_encodable_at(certification_version), ) ) -> Stream { stream @@ -961,8 +982,14 @@ prop_compose! { prop_compose! { /// Produces a strategy consisting of an arbitrary stream and valid slice begin and message /// count values for extracting a slice from the stream. - pub fn arb_stream_slice(min_size: usize, max_size: usize, min_signal_count: usize, max_signal_count: usize)( - stream in arb_stream(min_size, max_size, min_signal_count, max_signal_count), + pub fn arb_stream_slice( + min_size: usize, + max_size: usize, + min_signal_count: usize, + max_signal_count: usize, + certification_version: CertificationVersion, + )( + stream in arb_stream(min_size, max_size, min_signal_count, max_signal_count, certification_version), from_percent in -20..120_i64, percent_above_min_size in 0..120_i64, ) -> (Stream, StreamIndex, usize) { @@ -1011,9 +1038,10 @@ prop_compose! { pub fn arb_invalid_stream_header( min_signal_count: usize, max_signal_count: usize, + with_reject_reasons: Vec, )( - valid_stream_header in arb_stream_header(min_signal_count, max_signal_count, RejectReason::all()), - reason in proptest::sample::select(RejectReason::all()), + valid_stream_header in arb_stream_header(min_signal_count, max_signal_count, with_reject_reasons.clone()), + reason in proptest::sample::select(with_reject_reasons), ) -> StreamHeader { let begin = valid_stream_header.begin(); let end = valid_stream_header.end(); diff --git a/rs/types/types/src/xnet.rs b/rs/types/types/src/xnet.rs index 30393a46e404..83d59c327a94 100644 --- a/rs/types/types/src/xnet.rs +++ b/rs/types/types/src/xnet.rs @@ -211,6 +211,10 @@ pub enum RejectReason { /// `StateError` variants that shouldn't be possible to occur for requests. /// It is not expected that this reason will ever be used. Unknown = 7, + + /// Request rejected because either the sending or receiving subnet is a CloudEngine subnet, + /// which does not allow guaranteed-response calls or attached cycles. + EngineNotAllowed = 8, } impl RejectReason { @@ -231,6 +235,7 @@ impl From for pb_queues::RejectReason { RejectReason::QueueFull => Self::QueueFull, RejectReason::OutOfMemory => Self::OutOfMemory, RejectReason::Unknown => Self::Unknown, + RejectReason::EngineNotAllowed => Self::EngineNotAllowed, } } } @@ -250,6 +255,7 @@ impl TryFrom for RejectReason { pb_queues::RejectReason::QueueFull => Ok(Self::QueueFull), pb_queues::RejectReason::OutOfMemory => Ok(Self::OutOfMemory), pb_queues::RejectReason::Unknown => Ok(Self::Unknown), + pb_queues::RejectReason::EngineNotAllowed => Ok(Self::EngineNotAllowed), } } } diff --git a/rs/xnet/payload_builder/tests/certified_slice_pool.rs b/rs/xnet/payload_builder/tests/certified_slice_pool.rs index 396f0c1ef2a0..bf9a11acbea8 100644 --- a/rs/xnet/payload_builder/tests/certified_slice_pool.rs +++ b/rs/xnet/payload_builder/tests/certified_slice_pool.rs @@ -1,5 +1,5 @@ use assert_matches::assert_matches; -use ic_canonical_state::LabelLike; +use ic_canonical_state::{CURRENT_CERTIFICATION_VERSION, LabelLike}; use ic_crypto_tree_hash::{Label, LabeledTree, flat_map::FlatMap}; use ic_interfaces_certified_stream_store::DecodeStreamError; use ic_interfaces_certified_stream_store_mocks::MockCertifiedStreamStore; @@ -40,6 +40,7 @@ fn slice_unpack_roundtrip( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -63,6 +64,7 @@ fn slice_garbage_collect( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -140,6 +142,7 @@ fn slice_take_prefix( 100, // max_size 0, // min_signal_count 100, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -284,6 +287,7 @@ fn invalid_slice( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -421,6 +425,7 @@ fn slice_accurate_count_bytes( 100, // max_size 0, // min_signal_count 100, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -491,6 +496,7 @@ fn matching_count_bytes( 100, // max_size 0, // min_signal_count 100, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -524,6 +530,7 @@ fn pool( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -790,6 +797,7 @@ fn pool_append_same_slice( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -907,6 +915,7 @@ fn pool_append_non_empty_to_empty( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -975,6 +984,7 @@ fn pool_append_non_empty_to_non_empty( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -1096,6 +1106,7 @@ fn pool_put_invalid_slice( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -1151,6 +1162,7 @@ fn pool_append_invalid_slice( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -1240,6 +1252,7 @@ fn pool_append_invalid_slice_to_empty( 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -1299,6 +1312,7 @@ fn pool_take_slice_respects_signal_limit( 2 * MAX_SIGNALS, // max_size 0, // min_signal_count 0, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { diff --git a/rs/xnet/payload_builder/tests/xnet_payload_builder.rs b/rs/xnet/payload_builder/tests/xnet_payload_builder.rs index 8561d58d4fab..0284650cb7ec 100644 --- a/rs/xnet/payload_builder/tests/xnet_payload_builder.rs +++ b/rs/xnet/payload_builder/tests/xnet_payload_builder.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use ic_base_types::NumBytes; +use ic_canonical_state::CURRENT_CERTIFICATION_VERSION; use ic_interfaces::messaging::{XNetPayloadBuilder, XNetPayloadValidationError}; use ic_interfaces_certified_stream_store::{CertifiedStreamStore, DecodeStreamError}; use ic_interfaces_certified_stream_store_mocks::MockCertifiedStreamStore; @@ -22,7 +23,9 @@ use ic_test_utilities_metrics::{ fetch_int_counter_vec, metric_vec, }; use ic_test_utilities_registry::SubnetRecordBuilder; -use ic_test_utilities_state::{arb_stream, arb_stream_slice, arb_stream_with_config}; +use ic_test_utilities_state::{ + arb_stream, arb_stream_slice, arb_stream_with_config, reject_reasons_encodable_at, +}; use ic_test_utilities_types::ids::{ NODE_1, NODE_2, NODE_3, NODE_4, NODE_5, NODE_42, SUBNET_1, SUBNET_2, SUBNET_3, SUBNET_4, SUBNET_5, @@ -30,9 +33,7 @@ use ic_test_utilities_types::ids::{ use ic_test_utilities_types::messages::RequestBuilder; use ic_types::batch::{ValidationContext, XNetPayload}; use ic_types::time::UNIX_EPOCH; -use ic_types::xnet::{ - CertifiedStreamSlice, RejectReason, StreamIndex, StreamIndexedQueue, StreamSlice, -}; +use ic_types::xnet::{CertifiedStreamSlice, StreamIndex, StreamIndexedQueue, StreamSlice}; use ic_types::{CountBytes, Height, NodeId, RegistryVersion, SubnetId}; use ic_xnet_payload_builder::certified_slice_pool::{CertifiedSlicePool, UnpackedStreamSlice}; use ic_xnet_payload_builder::testing::*; @@ -312,7 +313,7 @@ fn get_xnet_payload_respects_signal_limit( 30..=40, // size_range 10..=20, // signal_start_range (MAX_SIGNALS - 10)..=MAX_SIGNALS, // signal_count_range - RejectReason::all(), + reject_reasons_encodable_at(CURRENT_CERTIFICATION_VERSION), ))] out_stream: Stream, @@ -322,7 +323,7 @@ fn get_xnet_payload_respects_signal_limit( (MAX_SIGNALS + 20)..=(MAX_SIGNALS + 30), // size_range 10..=20, // signal_start_range 0..=10, // signal_count_range - RejectReason::all(), + reject_reasons_encodable_at(CURRENT_CERTIFICATION_VERSION), ))] in_stream: Stream, ) { @@ -382,6 +383,7 @@ fn get_xnet_payload_slice_alignment( 5, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -519,6 +521,7 @@ fn get_xnet_payload_just_under_byte_limit( 3, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice1: (Stream, StreamIndex, usize), #[strategy(arb_stream_slice( @@ -526,6 +529,7 @@ fn get_xnet_payload_just_under_byte_limit( 3, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice2: (Stream, StreamIndex, usize), ) { @@ -577,6 +581,7 @@ fn get_xnet_payload_byte_limit_exceeded( 3, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice1: (Stream, StreamIndex, usize), #[strategy(arb_stream_slice( @@ -584,6 +589,7 @@ fn get_xnet_payload_byte_limit_exceeded( 3, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice2: (Stream, StreamIndex, usize), #[strategy(0..100_usize)] message_bytes_percentage: usize, @@ -642,6 +648,7 @@ fn get_xnet_payload_byte_limit_too_small( 3, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -689,6 +696,7 @@ fn get_xnet_payload_empty_slice( 1, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] out_stream: Stream, ) { @@ -768,6 +776,7 @@ fn system_subnet_stream_throttling( SYSTEM_SUBNET_STREAM_MSG_LIMIT + 10, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] out_stream: Stream, #[strategy(arb_stream_slice( @@ -775,6 +784,7 @@ fn system_subnet_stream_throttling( SYSTEM_SUBNET_STREAM_MSG_LIMIT, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -862,6 +872,7 @@ fn validate_xnet_payload( 3, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice1: (Stream, StreamIndex, usize), #[strategy(arb_stream_slice( @@ -869,6 +880,7 @@ fn validate_xnet_payload( 3, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice2: (Stream, StreamIndex, usize), #[strategy(0..110_u64)] size_limit_percentage: u64, @@ -965,6 +977,7 @@ fn refill_pool_empty( 5, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -1066,6 +1079,7 @@ fn refill_pool_append( 5, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -1195,6 +1209,7 @@ fn refill_pool_put_invalid_slice( 5, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { @@ -1295,6 +1310,7 @@ fn refill_pool_append_invalid_slice( 5, // max_size 0, // min_signal_count 10, // max_signal_count + CURRENT_CERTIFICATION_VERSION, ))] test_slice: (Stream, StreamIndex, usize), ) { From 0b84029c3f8c71db7d948b2aa1d5e4734e978e2d Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Thu, 11 Jun 2026 21:07:02 +0200 Subject: [PATCH 23/75] chore(systests): force testnet allocation to the local DC for all system-tests (#10436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What Force Farm testnet allocation to the same DC as the GitHub runner executing the test (which is also the DC holding the just-built images) — for **every** system-test. This generalizes the opt-in `.allocate_testnet_to_local_dc()` mechanism introduced in #10122. # Why #10122 showed that cross-DC transfers of large images (e.g. 2.6G SetupOS images from `dm1` to `zh1` or vice versa) cause download timeouts and flaky tests. The same applies to all system-tests, so testnets should always be allocated in the DC where the test runs and the images live. # How * Replace the `allocate_testnet_to_local_dc` bool on `SystemTestGroup` (and its builder method) with the `ALLOCATE_TESTNET_TO_LOCAL_DC` environment variable, read by the test driver in `create_group_setup` (accepted values: `1`/`true`/`0`/`false`). * Set `ALLOCATE_TESTNET_TO_LOCAL_DC=1` unconditionally from the `system_test` macro in `rs/tests/system_tests.bzl`, covering both the plain and the `_colocate` targets. This remains a no-op when the `DC` volatile status variable is unknown (e.g. local runs without `NODE_NAME`). * Drop the now-redundant `.allocate_testnet_to_local_dc()` calls from the 7 nested system-tests. * Replace `dep_download_url` in `rs/tests/upload_systest_dep.sh` to no longer go via the dc_http_proxy but point directly at the DC-local bazel cache: `https://artifacts.$cluster.dfinity.network/cas/$dep_sha256`. * Force the `release-system-tests` job in `release-testing.yml` and the `system-tests-benchmarks-nightly` job to run in runner group `dm1` (the `&dind-large-setup` anchor moved to `setup-guest-os-qualification`, whose jobs keep their current runners). # Notes for reviewers * Pinning all tests to the runner's DC concentrates Farm load in `dm1` where most runners live; watch for allocation failures after rollout. Extra charts have been added to the [Farm Dashboard](https://grafana.dm1-idx1.dfinity.network/d/uwEFG_yGk/farm-dashboard?from=now-3h&to=now&timezone=utc&refresh=10s) for monitoring dm1 and zh1. * Farm hosts/UVMs now fetch deps from `artifacts..dfinity.network` over HTTPS (previously plain http via proxy-global:8080); the redirect server already returns URLs of this form. --- .github/workflows/release-testing.yml | 6 ++- .../system-tests-benchmarks-nightly.yml | 1 + rs/tests/driver/src/driver/group.rs | 13 +------ rs/tests/driver/src/driver/test_env_api.rs | 37 ++++++++++++------- rs/tests/nested/guestos_upgrade.rs | 1 - rs/tests/nested/hostos_upgrade.rs | 1 - .../nr_all_broken_seq_np_actions.rs | 1 - .../nns_recovery/nr_broken_dfinity_node.rs | 1 - rs/tests/nested/nns_recovery/nr_local.rs | 1 - .../nns_recovery/nr_no_bless_fix_like_np.rs | 1 - rs/tests/nested/registration.rs | 1 - rs/tests/system_tests.bzl | 6 +++ rs/tests/upload_systest_dep.sh | 4 +- 13 files changed, 38 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release-testing.yml b/.github/workflows/release-testing.yml index b142f98c7238..594fcf9006e1 100644 --- a/.github/workflows/release-testing.yml +++ b/.github/workflows/release-testing.yml @@ -31,7 +31,8 @@ jobs: # as a separate job. release-system-tests: name: Release System Tests - runs-on: &dind-large-setup + runs-on: + group: dm1 labels: dind-large container: &container-setup image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 @@ -67,7 +68,8 @@ jobs: setup-guest-os-qualification: name: Setting up guest os qualification pipeline - runs-on: *dind-large-setup + runs-on: &dind-large-setup + labels: dind-large container: *container-setup timeout-minutes: 180 outputs: diff --git a/.github/workflows/system-tests-benchmarks-nightly.yml b/.github/workflows/system-tests-benchmarks-nightly.yml index 18681ac2fafd..61943755ef90 100644 --- a/.github/workflows/system-tests-benchmarks-nightly.yml +++ b/.github/workflows/system-tests-benchmarks-nightly.yml @@ -14,6 +14,7 @@ jobs: system-tests-benchmarks-nightly: name: Bazel System Test Benchmarks runs-on: + group: dm1 labels: dind-large container: image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 diff --git a/rs/tests/driver/src/driver/group.rs b/rs/tests/driver/src/driver/group.rs index eb5d2855c75d..0d3b3632b4b3 100644 --- a/rs/tests/driver/src/driver/group.rs +++ b/rs/tests/driver/src/driver/group.rs @@ -690,7 +690,6 @@ pub fn assert_no_critical_errors(env: &TestEnv) { } pub struct SystemTestGroup { - allocate_testnet_to_local_dc: bool, setup: Option>, teardowns: Vec>, tests: Vec, @@ -735,7 +734,6 @@ impl TestEnvAttribute for CliArguments { impl SystemTestGroup { pub fn new() -> Self { Self { - allocate_testnet_to_local_dc: false, setup: Default::default(), teardowns: Default::default(), tests: Default::default(), @@ -776,11 +774,6 @@ impl SystemTestGroup { self } - pub fn allocate_testnet_to_local_dc(mut self) -> Self { - self.allocate_testnet_to_local_dc = true; - self - } - pub fn with_setup(mut self, setup: F) -> Self { self.setup = Some(Box::new(setup)); self @@ -1328,11 +1321,7 @@ impl SystemTestGroup { } InfraProvider::Farm.write_attribute(&root_env); if with_farm { - root_env.create_group_setup( - group_ctx.group_base_name.clone(), - self.allocate_testnet_to_local_dc, - args.no_group_ttl, - ); + root_env.create_group_setup(group_ctx.group_base_name.clone(), args.no_group_ttl); } debug!(group_ctx.log(), "Created group context: {:?}", group_ctx); } diff --git a/rs/tests/driver/src/driver/test_env_api.rs b/rs/tests/driver/src/driver/test_env_api.rs index 7c06d9df1fa3..5da1fba0715b 100644 --- a/rs/tests/driver/src/driver/test_env_api.rs +++ b/rs/tests/driver/src/driver/test_env_api.rs @@ -1428,12 +1428,7 @@ pub fn get_build_setupos_config_image_tool() -> PathBuf { } pub trait HasGroupSetup { - fn create_group_setup( - &self, - group_base_name: String, - allocate_testnet_to_local_dc: bool, - no_group_ttl: bool, - ); + fn create_group_setup(&self, group_base_name: String, no_group_ttl: bool); } /// Name of the environment variable that controls the VM allocation mode used @@ -1455,13 +1450,29 @@ fn vm_allocation_mode_from_env() -> Option { Some(mode) } +/// Name of the environment variable that controls whether the Farm group is +/// created with a required host feature restricting allocation to the local +/// DC, i.e. the DC of the machine running the test as specified by the `DC` +/// environment variable. Accepted values are `1`/`true` and `0`/`false`. +const ALLOCATE_TESTNET_TO_LOCAL_DC_ENV_VAR: &str = "ALLOCATE_TESTNET_TO_LOCAL_DC"; + +fn allocate_testnet_to_local_dc_from_env() -> bool { + let raw = match std::env::var(ALLOCATE_TESTNET_TO_LOCAL_DC_ENV_VAR) { + Ok(v) if !v.is_empty() => v, + _ => return false, + }; + match raw.as_str() { + "1" | "true" => true, + "0" | "false" => false, + _ => panic!( + "Invalid value {raw:?} for environment variable {ALLOCATE_TESTNET_TO_LOCAL_DC_ENV_VAR}: \ + accepted values are \"1\", \"true\", \"0\" and \"false\"" + ), + } +} + impl HasGroupSetup for TestEnv { - fn create_group_setup( - &self, - group_base_name: String, - allocate_testnet_to_local_dc: bool, - no_group_ttl: bool, - ) { + fn create_group_setup(&self, group_base_name: String, no_group_ttl: bool) { let log = self.logger(); let vm_allocation_mode = vm_allocation_mode_from_env(); if GroupSetup::attribute_exists(self) { @@ -1477,7 +1488,7 @@ impl HasGroupSetup for TestEnv { let group_setup = GroupSetup::new(group_base_name.clone(), timeout); match InfraProvider::read_attribute(self) { InfraProvider::Farm => { - let required_host_features = allocate_testnet_to_local_dc + let required_host_features = allocate_testnet_to_local_dc_from_env() .then(|| std::env::var("DC").ok()) .flatten() .map(|dc| vec![HostFeature::DC(dc)]) diff --git a/rs/tests/nested/guestos_upgrade.rs b/rs/tests/nested/guestos_upgrade.rs index 34099983d48c..db89d6a48df8 100644 --- a/rs/tests/nested/guestos_upgrade.rs +++ b/rs/tests/nested/guestos_upgrade.rs @@ -22,7 +22,6 @@ use nested::{ fn main() -> Result<()> { SystemTestGroup::new() - .allocate_testnet_to_local_dc() .with_setup(nested::setup) .add_test(systest!(upgrade_guestos)) .with_timeout_per_test(Duration::from_secs(30 * 60)) diff --git a/rs/tests/nested/hostos_upgrade.rs b/rs/tests/nested/hostos_upgrade.rs index c1d0fe107165..fd7543ee21d7 100644 --- a/rs/tests/nested/hostos_upgrade.rs +++ b/rs/tests/nested/hostos_upgrade.rs @@ -17,7 +17,6 @@ use nested::util::{ fn main() -> Result<()> { SystemTestGroup::new() - .allocate_testnet_to_local_dc() .with_setup(nested::setup) .add_test(systest!(upgrade_hostos)) .with_timeout_per_test(Duration::from_secs(30 * 60)) diff --git a/rs/tests/nested/nns_recovery/nr_all_broken_seq_np_actions.rs b/rs/tests/nested/nns_recovery/nr_all_broken_seq_np_actions.rs index 0ccc3931b0e3..171019755890 100644 --- a/rs/tests/nested/nns_recovery/nr_all_broken_seq_np_actions.rs +++ b/rs/tests/nested/nns_recovery/nr_all_broken_seq_np_actions.rs @@ -32,7 +32,6 @@ use std::time::Duration; fn main() -> Result<()> { SystemTestGroup::new() - .allocate_testnet_to_local_dc() .with_setup(|env| { setup( env, diff --git a/rs/tests/nested/nns_recovery/nr_broken_dfinity_node.rs b/rs/tests/nested/nns_recovery/nr_broken_dfinity_node.rs index f918248c7665..775795451757 100644 --- a/rs/tests/nested/nns_recovery/nr_broken_dfinity_node.rs +++ b/rs/tests/nested/nns_recovery/nr_broken_dfinity_node.rs @@ -31,7 +31,6 @@ use std::time::Duration; fn main() -> Result<()> { SystemTestGroup::new() - .allocate_testnet_to_local_dc() .with_setup(|env| { setup( env, diff --git a/rs/tests/nested/nns_recovery/nr_local.rs b/rs/tests/nested/nns_recovery/nr_local.rs index 2d0269f725e3..117d2fcdf203 100644 --- a/rs/tests/nested/nns_recovery/nr_local.rs +++ b/rs/tests/nested/nns_recovery/nr_local.rs @@ -31,7 +31,6 @@ use std::time::Duration; fn main() -> Result<()> { SystemTestGroup::new() - .allocate_testnet_to_local_dc() .with_setup(|env| { setup( env, diff --git a/rs/tests/nested/nns_recovery/nr_no_bless_fix_like_np.rs b/rs/tests/nested/nns_recovery/nr_no_bless_fix_like_np.rs index 23102c3c2d12..efe94b94deb0 100644 --- a/rs/tests/nested/nns_recovery/nr_no_bless_fix_like_np.rs +++ b/rs/tests/nested/nns_recovery/nr_no_bless_fix_like_np.rs @@ -32,7 +32,6 @@ use std::time::Duration; fn main() -> Result<()> { SystemTestGroup::new() - .allocate_testnet_to_local_dc() .with_setup(|env| { setup( env, diff --git a/rs/tests/nested/registration.rs b/rs/tests/nested/registration.rs index 7eb8252c778a..cbc855d805b6 100644 --- a/rs/tests/nested/registration.rs +++ b/rs/tests/nested/registration.rs @@ -4,7 +4,6 @@ use std::time::Duration; fn main() -> Result<()> { SystemTestGroup::new() - .allocate_testnet_to_local_dc() .with_setup(nested::setup) .add_test(systest!(nested::registration)) .with_timeout_per_test(Duration::from_secs(20 * 60)) diff --git a/rs/tests/system_tests.bzl b/rs/tests/system_tests.bzl index 16adb7268d0c..d7caecdc127c 100644 --- a/rs/tests/system_tests.bzl +++ b/rs/tests/system_tests.bzl @@ -231,6 +231,12 @@ def system_test( env["RUN_SCRIPT_VOLATILE_STATUS_PATH"] = "$(rootpath //bazel:volatile-status.txt)" data.append("//bazel:volatile-status.txt") + # Make the test driver allocate the Farm testnet to the same DC as the + # machine running the test (the DC volatile status variable derived from + # NODE_NAME). This avoids slow cross-DC transfers of large images. + # No-op when the DC is unknown, e.g. when running locally. + env["ALLOCATE_TESTNET_TO_LOCAL_DC"] = "1" + sh_test( name = test_name, srcs = ["//rs/tests:run_systest.sh"], diff --git a/rs/tests/upload_systest_dep.sh b/rs/tests/upload_systest_dep.sh index a1d9b6788b45..b721313f4dc3 100755 --- a/rs/tests/upload_systest_dep.sh +++ b/rs/tests/upload_systest_dep.sh @@ -105,7 +105,7 @@ fi echo "dep '$dep_filename': cluster is '$cluster'" >&2 -# Use the direct URL, without going through the redirect server -dep_download_url="http://$cluster.artifacts.proxy-global.dfinity.network:8080/cas/$dep_sha256" +# Use the DC-local bazel cache directly, without going through the redirect server +dep_download_url="https://artifacts.$cluster.dfinity.network/cas/$dep_sha256" echo "dep '$dep_filename': download_url: '$dep_download_url'" >&2 echo "$dep_download_url" From 242ced10364b303f9a2051f60efbf2d0c4636865 Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic <73236646+NikolaMilosa@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:44:11 +0200 Subject: [PATCH 24/75] feat: allow engine-controller canister to update cloud engines directly (#10431) The users should be able to control their own settings for the engines they use. For now we are scoping that to only `subnet_admins` and `replica_version_id` but in the future we might want to allow more things. --------- Co-authored-by: IDX GitHub Automation Co-authored-by: pietrodimarco-dfinity <124565147+pietrodimarco-dfinity@users.noreply.github.com> --- rs/engine_controller/canister/canister.rs | 177 ++++++++++- rs/engine_controller/canister/tests.rs | 140 +++++++++ rs/engine_controller/engine_controller.did | 88 ++++++ rs/engine_controller/src/lib.rs | 6 + rs/registry/canister/canister/canister.rs | 10 +- .../canister/src/common/test_helpers.rs | 80 ++++- .../do_deploy_guestos_to_all_subnet_nodes.rs | 128 +++++++- .../src/mutations/do_update_subnet.rs | 276 ++++++++++++++++-- .../canister/tests/common/test_helpers.rs | 5 + rs/registry/canister/unreleased_changelog.md | 7 + 10 files changed, 874 insertions(+), 43 deletions(-) diff --git a/rs/engine_controller/canister/canister.rs b/rs/engine_controller/canister/canister.rs index a91daab8ff15..724e9b01ec5e 100644 --- a/rs/engine_controller/canister/canister.rs +++ b/rs/engine_controller/canister/canister.rs @@ -7,7 +7,8 @@ use candid::Principal; use ic_base_types::{NodeId, PrincipalId, SubnetId}; use ic_cdk::{api::msg_caller, call::Call, init, post_upgrade, println, update}; use ic_engine_controller::{ - CreateEngineArgs, DeleteEngineArgs, EngineControllerInitArgs, NewSubnet, + CreateEngineArgs, DeleteEngineArgs, DeployGuestosToAllSubnetNodesPayload, + EngineControllerInitArgs, NewSubnet, UpdateSubnetPayload, }; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_protobuf::registry::subnet::v1::SubnetFeatures; @@ -186,6 +187,180 @@ async fn delete_engine(args: DeleteEngineArgs) -> Result<(), String> { response } +/// Validates that the only fields set on the proxied `UpdateSubnetPayload` +/// are the ones the engine controller is allowed to manage: `subnet_admins` +/// and `is_halted` (subnet halting / unhalting). Every other `Option<_>` +/// field must be `None`, and the single non-optional knob +/// (`set_gossip_config_to_default`) must hold its default value (`false`). +/// The required `subnet_id` is exempt because it merely identifies the target. +/// +/// This keeps the surface of `update_subnet` deliberately tiny: only the +/// fields the engine controller is intended to manage flow through. Adding a +/// new allowed field is a conscious, code-level decision. +fn ensure_only_allowed_fields_set(payload: &UpdateSubnetPayload) -> Result<(), String> { + let UpdateSubnetPayload { + subnet_id: _, + // The fields we allow. + subnet_admins: _, + is_halted: _, + + max_ingress_bytes_per_message, + max_ingress_bytes_per_block, + max_ingress_messages_per_block, + max_block_payload_size, + unit_delay_millis, + initial_notary_delay_millis, + dkg_interval_length, + dkg_dealings_per_block, + start_as_nns, + subnet_type, + halt_at_cup_height, + features, + resource_limits, + chain_key_config, + chain_key_signing_enable, + chain_key_signing_disable, + max_number_of_canisters, + ssh_readonly_access, + ssh_backup_access, + max_artifact_streams_per_peer, + max_chunk_wait_ms, + max_duplicity, + max_chunk_size, + receive_check_cache_size, + pfn_evaluation_period_ms, + registry_poll_period_ms, + retransmission_request_ms, + set_gossip_config_to_default, + } = payload; + + // Build up a list of fields the caller is trying to set so the error is + // actionable. The check is purely structural: any `Some(_)` (or a + // non-default bool) is treated as "the caller tried to update this". + let mut disallowed: Vec<&'static str> = vec![]; + macro_rules! check_none { + ($field:expr, $name:literal) => { + if $field.is_some() { + disallowed.push($name); + } + }; + } + check_none!( + max_ingress_bytes_per_message, + "max_ingress_bytes_per_message" + ); + check_none!(max_ingress_bytes_per_block, "max_ingress_bytes_per_block"); + check_none!( + max_ingress_messages_per_block, + "max_ingress_messages_per_block" + ); + check_none!(max_block_payload_size, "max_block_payload_size"); + check_none!(unit_delay_millis, "unit_delay_millis"); + check_none!(initial_notary_delay_millis, "initial_notary_delay_millis"); + check_none!(dkg_interval_length, "dkg_interval_length"); + check_none!(dkg_dealings_per_block, "dkg_dealings_per_block"); + check_none!(start_as_nns, "start_as_nns"); + check_none!(subnet_type, "subnet_type"); + check_none!(halt_at_cup_height, "halt_at_cup_height"); + check_none!(features, "features"); + check_none!(resource_limits, "resource_limits"); + check_none!(chain_key_config, "chain_key_config"); + check_none!(chain_key_signing_enable, "chain_key_signing_enable"); + check_none!(chain_key_signing_disable, "chain_key_signing_disable"); + check_none!(max_number_of_canisters, "max_number_of_canisters"); + check_none!(ssh_readonly_access, "ssh_readonly_access"); + check_none!(ssh_backup_access, "ssh_backup_access"); + check_none!( + max_artifact_streams_per_peer, + "max_artifact_streams_per_peer" + ); + check_none!(max_chunk_wait_ms, "max_chunk_wait_ms"); + check_none!(max_duplicity, "max_duplicity"); + check_none!(max_chunk_size, "max_chunk_size"); + check_none!(receive_check_cache_size, "receive_check_cache_size"); + check_none!(pfn_evaluation_period_ms, "pfn_evaluation_period_ms"); + check_none!(registry_poll_period_ms, "registry_poll_period_ms"); + check_none!(retransmission_request_ms, "retransmission_request_ms"); + if *set_gossip_config_to_default { + disallowed.push("set_gossip_config_to_default"); + } + + if disallowed.is_empty() { + Ok(()) + } else { + Err(format!( + "Updating these fields via the engine controller is not allowed: {}. \ + Only `subnet_admins` and `is_halted` may be updated.", + disallowed.join(", ") + )) + } +} + +/// Ensures that the configured `AUTHORIZED_CALLER` (the engine controller's +/// "super admin") is always present in the resulting admin list, even if the +/// caller forgot to include it. +fn normalize_subnet_admins(admins: Vec) -> Vec { + let super_admin = PrincipalId(AUTHORIZED_CALLER.with(|c| *c.borrow())); + let mut admins = admins; + if !admins.contains(&super_admin) { + admins.push(super_admin); + } + admins +} + +/// Proxies to the registry's `update_subnet` endpoint. Only `subnet_admins` +/// and `is_halted` may be updated through this path; every other field must be +/// left at its default value (`None` / `false`) or the call is rejected. +/// +/// The `subnet_admins` list is always normalized to include the engine +/// controller's authorized caller (the super admin), so callers cannot +/// accidentally lock the controller out of the subnet. +#[update] +async fn update_subnet(payload: UpdateSubnetPayload) -> Result<(), String> { + ensure_authorized()?; + ensure_only_allowed_fields_set(&payload)?; + + // Normalize `subnet_admins` so the super admin is always present. + // The caller may omit the field entirely (no change requested), but if + // they do supply one, we treat it as the source of truth and add the + // super admin if missing. + #[allow(unused_mut)] + let mut payload = payload; + if let Some(admins) = payload.subnet_admins { + payload.subnet_admins = Some(normalize_subnet_admins(admins)); + } + + Call::unbounded_wait(REGISTRY_CANISTER_ID.into(), "update_subnet") + .with_arg(payload) + .await + .map_err(|e| format!("registry.update_subnet call failed: {e:?}"))? + .candid::<()>() + .map_err(|e| format!("Failed to decode registry response: {e}"))?; + + Ok(()) +} + +/// Proxies to the registry's `deploy_guestos_to_all_subnet_nodes` endpoint, +/// which is the registry path for updating a subnet's replica version. +#[update] +async fn deploy_guestos_to_all_subnet_nodes( + payload: DeployGuestosToAllSubnetNodesPayload, +) -> Result<(), String> { + ensure_authorized()?; + + Call::unbounded_wait( + REGISTRY_CANISTER_ID.into(), + "deploy_guestos_to_all_subnet_nodes", + ) + .with_arg(payload) + .await + .map_err(|e| format!("registry.deploy_guestos_to_all_subnet_nodes call failed: {e:?}"))? + .candid::<()>() + .map_err(|e| format!("Failed to decode registry response: {e}"))?; + + Ok(()) +} + fn main() { // This block is intentionally left blank. } diff --git a/rs/engine_controller/canister/tests.rs b/rs/engine_controller/canister/tests.rs index 0cae6e0e5f5c..08af9914cc13 100644 --- a/rs/engine_controller/canister/tests.rs +++ b/rs/engine_controller/canister/tests.rs @@ -1,5 +1,46 @@ use super::*; use candid_parser::utils::{CandidSource, service_equal}; +use ic_base_types::{PrincipalId, SubnetId}; + +/// Builds an `UpdateSubnetPayload` where every optional field is `None` and +/// every non-optional knob has its default. The given `subnet_id` identifies +/// the target; the caller can flip individual fields on the returned struct +/// to set up a specific test case. +fn empty_update_payload() -> UpdateSubnetPayload { + UpdateSubnetPayload { + subnet_id: SubnetId::new(PrincipalId::new_user_test_id(1)), + max_ingress_bytes_per_message: None, + max_ingress_bytes_per_block: None, + max_ingress_messages_per_block: None, + max_block_payload_size: None, + unit_delay_millis: None, + initial_notary_delay_millis: None, + dkg_interval_length: None, + dkg_dealings_per_block: None, + start_as_nns: None, + subnet_type: None, + is_halted: None, + halt_at_cup_height: None, + features: None, + resource_limits: None, + chain_key_config: None, + chain_key_signing_enable: None, + chain_key_signing_disable: None, + max_number_of_canisters: None, + ssh_readonly_access: None, + ssh_backup_access: None, + subnet_admins: None, + max_artifact_streams_per_peer: None, + max_chunk_wait_ms: None, + max_duplicity: None, + max_chunk_size: None, + receive_check_cache_size: None, + pfn_evaluation_period_ms: None, + registry_poll_period_ms: None, + retransmission_request_ms: None, + set_gossip_config_to_default: false, + } +} /// This is NOT affected by /// @@ -32,3 +73,102 @@ fn test_implemented_interface_matches_declared_interface_exactly() { let result = service_equal(declared_interface, implemented_interface); assert!(result.is_ok(), "{:?}\n\n", result.unwrap_err()); } + +#[test] +fn ensure_only_allowed_fields_set_accepts_empty_payload() { + // Pure no-op: no fields set at all. We don't actually want to allow this + // in practice (the call would be useless), but the validator's job is + // purely structural — it must accept any payload where the only mutated + // fields are the allowed ones. + ensure_only_allowed_fields_set(&empty_update_payload()) + .expect("an empty payload must pass the structural check"); +} + +#[test] +fn ensure_only_allowed_fields_set_accepts_subnet_admins_only() { + let mut payload = empty_update_payload(); + payload.subnet_admins = Some(vec![PrincipalId::new_user_test_id(42)]); + ensure_only_allowed_fields_set(&payload).expect("subnet_admins-only payload must be allowed"); +} + +#[test] +fn ensure_only_allowed_fields_set_accepts_is_halted() { + for is_halted in [true, false] { + let mut payload = empty_update_payload(); + payload.is_halted = Some(is_halted); + ensure_only_allowed_fields_set(&payload) + .unwrap_or_else(|e| panic!("is_halted={is_halted} payload must be allowed: {e}")); + } +} + +#[test] +fn ensure_only_allowed_fields_set_accepts_subnet_admins_and_is_halted() { + let mut payload = empty_update_payload(); + payload.subnet_admins = Some(vec![PrincipalId::new_user_test_id(42)]); + payload.is_halted = Some(true); + ensure_only_allowed_fields_set(&payload) + .expect("subnet_admins + is_halted payload must be allowed"); +} + +#[test] +fn ensure_only_allowed_fields_set_rejects_other_fields() { + let mut payload = empty_update_payload(); + payload.max_number_of_canisters = Some(100); + // `halt_at_cup_height` is intentionally *not* part of the allowed surface, + // even though it is halting-adjacent: only `is_halted` is. + payload.halt_at_cup_height = Some(true); + let err = ensure_only_allowed_fields_set(&payload).expect_err("should reject"); + assert!( + err.contains("max_number_of_canisters"), + "error must mention disallowed field: {err}" + ); + assert!( + err.contains("halt_at_cup_height"), + "error must mention disallowed field: {err}" + ); +} + +#[test] +fn ensure_only_allowed_fields_set_rejects_non_default_gossip_flag() { + let mut payload = empty_update_payload(); + payload.set_gossip_config_to_default = true; + let err = ensure_only_allowed_fields_set(&payload).expect_err("should reject"); + assert!( + err.contains("set_gossip_config_to_default"), + "error must mention the non-default bool: {err}" + ); +} + +#[test] +fn normalize_subnet_admins_adds_super_admin_when_missing() { + let other = PrincipalId::new_user_test_id(7); + let normalized = normalize_subnet_admins(vec![other]); + + let super_admin = PrincipalId(default_authorized_caller()); + assert!( + normalized.contains(&super_admin), + "super admin must be present after normalization" + ); + assert!( + normalized.contains(&other), + "other admins must be preserved" + ); +} + +#[test] +fn normalize_subnet_admins_keeps_list_intact_when_super_admin_present() { + let super_admin = PrincipalId(default_authorized_caller()); + let other = PrincipalId::new_user_test_id(8); + let input = vec![other, super_admin]; + let normalized = normalize_subnet_admins(input.clone()); + assert_eq!( + normalized, input, + "list must not be reordered or duplicated when super admin is already present" + ); +} + +#[test] +fn normalize_subnet_admins_handles_empty_input() { + let normalized = normalize_subnet_admins(vec![]); + assert_eq!(normalized, vec![PrincipalId(default_authorized_caller())]); +} diff --git a/rs/engine_controller/engine_controller.did b/rs/engine_controller/engine_controller.did index 71d96a68ac0d..153c0f23bf30 100644 --- a/rs/engine_controller/engine_controller.did +++ b/rs/engine_controller/engine_controller.did @@ -21,7 +21,95 @@ type EngineControllerInitArgs = record { initial_dkg_subnet_id : opt principal; }; +type SubnetType = variant { + application; + verified_application; + system; + cloud_engine; +}; + +type SubnetFeatures = record { + canister_sandboxing : bool; + http_requests : bool; + sev_enabled : opt bool; +}; + +type ResourceLimits = record { + maximum_state_size : opt nat64; + maximum_state_delta : opt nat64; +}; + +// Mirror of the subset of registry types referenced by UpdateSubnetPayload. +// The engine controller forwards the payload unchanged to the registry, but +// only `subnet_id` and `subnet_admins` may be set; every other field must be +// `null`/`false` or the call is rejected. +type EcdsaCurve = variant { secp256k1 }; +type EcdsaKeyId = record { curve : EcdsaCurve; name : text }; +type SchnorrAlgorithm = variant { bip340secp256k1; ed25519 }; +type SchnorrKeyId = record { algorithm : SchnorrAlgorithm; name : text }; +type VetKdCurve = variant { bls12_381_g2 }; +type VetKdKeyId = record { curve : VetKdCurve; name : text }; +type MasterPublicKeyId = variant { + Ecdsa : EcdsaKeyId; + Schnorr : SchnorrKeyId; + VetKd : VetKdKeyId; +}; + +type KeyConfig = record { + key_id : opt MasterPublicKeyId; + pre_signatures_to_create_in_advance : opt nat32; + max_queue_size : opt nat32; +}; + +type ChainKeyConfig = record { + key_configs : vec KeyConfig; + signature_request_timeout_ns : opt nat64; + idkg_key_rotation_period_ms : opt nat64; + max_parallel_pre_signature_transcripts_in_creation : opt nat32; +}; + +type UpdateSubnetPayload = record { + subnet_id : principal; + max_ingress_bytes_per_message : opt nat64; + max_ingress_bytes_per_block : opt nat64; + max_ingress_messages_per_block : opt nat64; + max_block_payload_size : opt nat64; + unit_delay_millis : opt nat64; + initial_notary_delay_millis : opt nat64; + dkg_interval_length : opt nat64; + dkg_dealings_per_block : opt nat64; + start_as_nns : opt bool; + subnet_type : opt SubnetType; + is_halted : opt bool; + halt_at_cup_height : opt bool; + features : opt SubnetFeatures; + resource_limits : opt ResourceLimits; + chain_key_config : opt ChainKeyConfig; + chain_key_signing_enable : opt vec MasterPublicKeyId; + chain_key_signing_disable : opt vec MasterPublicKeyId; + max_number_of_canisters : opt nat64; + ssh_readonly_access : opt vec text; + ssh_backup_access : opt vec text; + subnet_admins : opt vec principal; + max_artifact_streams_per_peer : opt nat32; + max_chunk_wait_ms : opt nat32; + max_duplicity : opt nat32; + max_chunk_size : opt nat32; + receive_check_cache_size : opt nat32; + pfn_evaluation_period_ms : opt nat32; + registry_poll_period_ms : opt nat32; + retransmission_request_ms : opt nat32; + set_gossip_config_to_default : bool; +}; + +type DeployGuestosToAllSubnetNodesPayload = record { + subnet_id : principal; + replica_version_id : text; +}; + service : (opt EngineControllerInitArgs) -> { create_engine : (CreateEngineArgs) -> (CreateEngineResult); delete_engine : (DeleteEngineArgs) -> (Result); + update_subnet : (UpdateSubnetPayload) -> (Result); + deploy_guestos_to_all_subnet_nodes : (DeployGuestosToAllSubnetNodesPayload) -> (Result); } diff --git a/rs/engine_controller/src/lib.rs b/rs/engine_controller/src/lib.rs index 6c79e52f4c4b..644bce74ba7b 100644 --- a/rs/engine_controller/src/lib.rs +++ b/rs/engine_controller/src/lib.rs @@ -10,6 +10,12 @@ use serde::Deserialize; // have to depend on `registry-canister` directly just to decode it. pub use registry_canister::mutations::do_create_subnet::NewSubnet; +// Re-export the payload types accepted by the proxy endpoints +// (`update_subnet` and `deploy_guestos_to_all_subnet_nodes`) so clients +// don't have to depend on `registry-canister` directly to construct them. +pub use registry_canister::mutations::do_deploy_guestos_to_all_subnet_nodes::DeployGuestosToAllSubnetNodesPayload; +pub use registry_canister::mutations::do_update_subnet::UpdateSubnetPayload; + #[derive(Clone, Debug, Default, CandidType, Deserialize)] pub struct EngineControllerInitArgs { /// If `Some`, replaces the default authorized caller; if `None`, the diff --git a/rs/registry/canister/canister/canister.rs b/rs/registry/canister/canister/canister.rs index d128079e1def..b3517e71dd9c 100644 --- a/rs/registry/canister/canister/canister.rs +++ b/rs/registry/canister/canister/canister.rs @@ -553,13 +553,14 @@ fn revise_elected_replica_versions_(payload: ReviseElectedGuestosVersionsPayload #[unsafe(export_name = "canister_update deploy_guestos_to_all_subnet_nodes")] fn deploy_guestos_to_all_subnet_nodes() { - check_caller_is_governance_and_log("deploy_guestos_to_all_subnet_nodes"); + check_caller_is_governance_or_engine_controller_and_log("deploy_guestos_to_all_subnet_nodes"); over(candid_one, deploy_guestos_to_all_subnet_nodes_); } #[candid_method(update, rename = "deploy_guestos_to_all_subnet_nodes")] fn deploy_guestos_to_all_subnet_nodes_(payload: DeployGuestosToAllSubnetNodesPayload) { - registry_mut().do_deploy_guestos_to_all_subnet_nodes(payload); + let caller = dfn_core::api::caller(); + registry_mut().do_deploy_guestos_to_all_subnet_nodes(caller, payload); recertify_registry(); } @@ -869,7 +870,7 @@ fn remove_node_operators_(payload: RemoveNodeOperatorsPayload) { #[unsafe(export_name = "canister_update update_subnet")] fn update_subnet() { - check_caller_is_governance_and_log("update_subnet"); + check_caller_is_governance_or_engine_controller_and_log("update_subnet"); over(candid_one, |payload: UpdateSubnetPayload| { update_subnet_(payload) }); @@ -877,7 +878,8 @@ fn update_subnet() { #[candid_method(update, rename = "update_subnet")] fn update_subnet_(payload: UpdateSubnetPayload) { - registry_mut().do_update_subnet(payload); + let caller = dfn_core::api::caller(); + registry_mut().do_update_subnet(caller, payload); recertify_registry(); } diff --git a/rs/registry/canister/src/common/test_helpers.rs b/rs/registry/canister/src/common/test_helpers.rs index eca76cf75c39..ce8f6bc9cd01 100644 --- a/rs/registry/canister/src/common/test_helpers.rs +++ b/rs/registry/canister/src/common/test_helpers.rs @@ -4,18 +4,20 @@ use crate::mutations::node_management::do_add_node::connection_endpoint_from_str use crate::registry::Registry; use ic_base_types::{NodeId, PrincipalId, SubnetId}; use ic_nns_test_utils::registry::{ - create_subnet_threshold_signing_pubkey_and_cup_mutations, invariant_compliant_mutation, - new_node_keys_and_node_id, + TEST_ID, create_subnet_threshold_signing_pubkey_and_cup_mutations, + invariant_compliant_mutation, new_node_keys_and_node_id, }; use ic_protobuf::registry::crypto::v1::PublicKey; use ic_protobuf::registry::node::v1::NodeRecord; use ic_protobuf::registry::node::v1::{IPv4InterfaceConfig, NodeRewardType}; use ic_protobuf::registry::node_operator::v1::NodeOperatorRecord; -use ic_protobuf::registry::subnet::v1::SubnetListRecord; -use ic_protobuf::registry::subnet::v1::SubnetRecord; +use ic_protobuf::registry::subnet::v1::{ + CanisterCyclesCostSchedule, SubnetListRecord, SubnetRecord, +}; use ic_registry_keys::make_node_operator_record_key; use ic_registry_keys::make_subnet_list_record_key; use ic_registry_keys::make_subnet_record_key; +use ic_registry_subnet_type::SubnetType; use ic_registry_transport::pb::v1::{ RegistryAtomicMutateRequest, RegistryMutation, registry_mutation::Type, }; @@ -204,6 +206,76 @@ pub fn prepare_registry_raw( (mutate_request, node_ids_and_dkg_pks) } +/// Prepares the mutations that add a CloudEngine subnet to a registry that was +/// initialized with [`invariant_compliant_mutation`] / [`invariant_compliant_registry`]. +/// +/// This first creates `node_count` fresh type-4 nodes (CloudEngine subnets may +/// only contain type-4 nodes) and then a CloudEngine subnet record made up of +/// those nodes, on the `Free` cost schedule (both required for CloudEngine +/// subnets). The returned request is meant to be pushed as an additional init +/// mutate request on top of the invariant-compliant base; it also returns the +/// id of the new CloudEngine subnet. +pub fn prepare_registry_with_cloud_engine_subnet( + node_count: u64, + starting_mutation_id: u8, +) -> (RegistryAtomicMutateRequest, SubnetId) { + // CloudEngine subnets may only contain type-4 nodes (enforced by the + // `check_node_type4_iff_cloud_engine` invariant). + let (nodes_request, node_ids_and_dkg_pks) = prepare_registry_with_nodes_and_reward_type( + starting_mutation_id, + node_count, + NodeRewardType::Type4, + ); + let mut mutations = nodes_request.mutations; + + // CloudEngine subnets are not charged cycles, i.e. they use the `Free` cost + // schedule. + let subnet_record = SubnetRecord { + membership: node_ids_and_dkg_pks + .keys() + .map(|node_id| node_id.get().to_vec()) + .collect(), + subnet_type: i32::from(SubnetType::CloudEngine), + canister_cycles_cost_schedule: i32::from(CanisterCyclesCostSchedule::Free), + replica_version_id: ReplicaVersion::default().to_string(), + unit_delay_millis: 600, + ..Default::default() + }; + + // The invariant-compliant base contains a single system subnet (`TEST_ID`); + // append the new CloudEngine subnet to the subnet list. + let cloud_engine_subnet_id = subnet_test_id(TEST_ID + 1); + let subnet_list_record = SubnetListRecord { + subnets: vec![ + subnet_test_id(TEST_ID).get().to_vec(), + cloud_engine_subnet_id.get().to_vec(), + ], + }; + + mutations.push(upsert( + make_subnet_list_record_key().as_bytes(), + subnet_list_record.encode_to_vec(), + )); + mutations.push(upsert( + make_subnet_record_key(cloud_engine_subnet_id).as_bytes(), + subnet_record.encode_to_vec(), + )); + mutations.append( + &mut create_subnet_threshold_signing_pubkey_and_cup_mutations( + cloud_engine_subnet_id, + &node_ids_and_dkg_pks, + ), + ); + + ( + RegistryAtomicMutateRequest { + mutations, + preconditions: vec![], + }, + cloud_engine_subnet_id, + ) +} + pub fn registry_create_subnet_with_nodes( registry: &mut Registry, node_ids_and_dkg_pks: &BTreeMap, diff --git a/rs/registry/canister/src/mutations/do_deploy_guestos_to_all_subnet_nodes.rs b/rs/registry/canister/src/mutations/do_deploy_guestos_to_all_subnet_nodes.rs index c72db03a65f5..3901908b74a2 100644 --- a/rs/registry/canister/src/mutations/do_deploy_guestos_to_all_subnet_nodes.rs +++ b/rs/registry/canister/src/mutations/do_deploy_guestos_to_all_subnet_nodes.rs @@ -6,7 +6,8 @@ use candid::{CandidType, Deserialize}; #[cfg(target_arch = "wasm32")] use dfn_core::println; use ic_base_types::{PrincipalId, SubnetId}; -use ic_protobuf::registry::subnet::v1::SubnetRecord; +use ic_nns_constants::ENGINE_CONTROLLER_CANISTER_ID; +use ic_protobuf::registry::subnet::v1::{SubnetRecord, SubnetType as SubnetTypePb}; use ic_registry_keys::make_subnet_record_key; use ic_registry_transport::pb::v1::{RegistryMutation, RegistryValue, registry_mutation}; use prost::Message; @@ -15,14 +16,32 @@ use serde::Serialize; impl Registry { pub fn do_deploy_guestos_to_all_subnet_nodes( &mut self, + caller: PrincipalId, payload: DeployGuestosToAllSubnetNodesPayload, ) { - println!("{LOG_PREFIX}do_deploy_guestos_to_all_subnet_nodes: {payload:?}"); + println!( + "{LOG_PREFIX}do_deploy_guestos_to_all_subnet_nodes: caller={caller}, payload={payload:?}" + ); + + let subnet_id = SubnetId::from(payload.subnet_id); + + // The engine controller canister is only allowed to mutate CloudEngine + // subnets. Other authorized callers (governance) can update any subnet. + if caller == ENGINE_CONTROLLER_CANISTER_ID.get() { + let subnet_record = self.get_subnet_or_panic(subnet_id); + assert_eq!( + subnet_record.subnet_type, + i32::from(SubnetTypePb::CloudEngine), + "{LOG_PREFIX}do_deploy_guestos_to_all_subnet_nodes: engine controller may only \ + deploy GuestOS to CloudEngine subnets; subnet {subnet_id} has subnet_type {:?}", + subnet_record.subnet_type, + ); + } check_replica_version_is_elected(self, &payload.replica_version_id); // Get the subnet record - let subnet_key = make_subnet_record_key(SubnetId::from(payload.subnet_id)); + let subnet_key = make_subnet_record_key(subnet_id); let mutation = match self.get(subnet_key.as_bytes(), self.latest_version()) { Some(RegistryValue { value: subnet_record_vec, @@ -48,6 +67,109 @@ impl Registry { } } +#[cfg(test)] +mod tests { + use super::*; + use crate::common::test_helpers::{ + add_fake_subnet, get_invariant_compliant_subnet_record, invariant_compliant_registry, + prepare_registry_with_cloud_engine_subnet, prepare_registry_with_nodes, + }; + use ic_nns_constants::{ENGINE_CONTROLLER_CANISTER_ID, GOVERNANCE_CANISTER_ID}; + use ic_protobuf::registry::subnet::v1::SubnetType as SubnetTypePb; + use ic_registry_subnet_type::SubnetType; + use ic_test_utilities_types::ids::subnet_test_id; + use ic_types::ReplicaVersion; + use maplit::btreemap; + + /// Creates a registry with a single non-CloudEngine subnet of the given + /// `subnet_type`. For CloudEngine subnets, use + /// [`prepare_registry_with_cloud_engine_subnet`] directly. + fn make_registry_with_non_cloud_engine_subnet(subnet_type: SubnetType) -> (Registry, SubnetId) { + assert_ne!( + subnet_type, + SubnetType::CloudEngine, + "use prepare_registry_with_cloud_engine_subnet for CloudEngine subnets", + ); + let mut registry = invariant_compliant_registry(0); + let (mutate_request, node_ids_and_dkg_pks) = prepare_registry_with_nodes(1, 2); + registry.maybe_apply_mutation_internal(mutate_request.mutations); + + let mut subnet_list_record = registry.get_subnet_list_record(); + + let (first_node_id, first_dkg_pk) = node_ids_and_dkg_pks + .iter() + .next() + .expect("should contain at least one node ID"); + + let mut subnet_record = get_invariant_compliant_subnet_record(vec![*first_node_id]); + subnet_record.subnet_type = i32::from(SubnetTypePb::from(subnet_type)); + + let subnet_id = subnet_test_id(3000); + registry.maybe_apply_mutation_internal(add_fake_subnet( + subnet_id, + &mut subnet_list_record, + subnet_record, + &btreemap!(*first_node_id => first_dkg_pk.clone()), + )); + + (registry, subnet_id) + } + + fn deploy_payload(subnet_id: SubnetId) -> DeployGuestosToAllSubnetNodesPayload { + DeployGuestosToAllSubnetNodesPayload { + subnet_id: subnet_id.get(), + replica_version_id: ReplicaVersion::default().to_string(), + } + } + + #[test] + fn engine_controller_can_deploy_to_cloud_engine_subnet() { + let mut registry = invariant_compliant_registry(0); + let (mutate_request, subnet_id) = prepare_registry_with_cloud_engine_subnet(1, 2); + registry.maybe_apply_mutation_internal(mutate_request.mutations); + + registry.do_deploy_guestos_to_all_subnet_nodes( + ENGINE_CONTROLLER_CANISTER_ID.get(), + deploy_payload(subnet_id), + ); + + let subnet_record = registry.get_subnet_or_panic(subnet_id); + assert_eq!( + subnet_record.replica_version_id, + ReplicaVersion::default().to_string() + ); + } + + #[test] + #[should_panic(expected = "engine controller may only deploy GuestOS to CloudEngine subnets")] + fn engine_controller_cannot_deploy_to_non_cloud_engine_subnet() { + let (mut registry, subnet_id) = + make_registry_with_non_cloud_engine_subnet(SubnetType::Application); + + registry.do_deploy_guestos_to_all_subnet_nodes( + ENGINE_CONTROLLER_CANISTER_ID.get(), + deploy_payload(subnet_id), + ); + } + + #[test] + fn governance_can_deploy_to_non_cloud_engine_subnet() { + let (mut registry, subnet_id) = + make_registry_with_non_cloud_engine_subnet(SubnetType::Application); + + registry.do_deploy_guestos_to_all_subnet_nodes( + GOVERNANCE_CANISTER_ID.get(), + deploy_payload(subnet_id), + ); + + let subnet_record = registry.get_subnet_or_panic(subnet_id); + assert_eq!( + subnet_record.replica_version_id, + ReplicaVersion::default().to_string() + ); + } +} + /// The argument of a command to update the replica version of a single subnet /// to a specific version. /// diff --git a/rs/registry/canister/src/mutations/do_update_subnet.rs b/rs/registry/canister/src/mutations/do_update_subnet.rs index 5e90e35d0e4f..b88f19e29f7d 100644 --- a/rs/registry/canister/src/mutations/do_update_subnet.rs +++ b/rs/registry/canister/src/mutations/do_update_subnet.rs @@ -3,10 +3,11 @@ use candid::{CandidType, Deserialize}; use dfn_core::println; use ic_base_types::{PrincipalId, SubnetId, subnet_id_into_protobuf}; use ic_management_canister_types_private::MasterPublicKeyId; +use ic_nns_constants::ENGINE_CONTROLLER_CANISTER_ID; use ic_protobuf::{ registry::subnet::v1::{ ResourceLimits as ResourceLimitsPb, SubnetFeatures as SubnetFeaturesPb, - SubnetRecord as SubnetRecordPb, + SubnetRecord as SubnetRecordPb, SubnetType as SubnetTypePb, }, types::v1::PrincipalId as PrincipalIdPb, }; @@ -22,17 +23,36 @@ use std::collections::HashSet; /// Updates the subnet's configuration in the registry. /// -/// This method is called by the governance canister, after a proposal -/// for updating a new subnet has been accepted. +/// This method is called by: +/// * the governance canister, after a proposal for updating a subnet has +/// been accepted (no scope restriction); and +/// * the engine controller canister, which may only target CloudEngine +/// subnets and is further restricted to a small subset of fields (see +/// [`ensure_engine_controller_payload_scope`]). impl Registry { - pub fn do_update_subnet(&mut self, payload: UpdateSubnetPayload) { - println!("{}do_update_subnet: {:?}", LOG_PREFIX, payload); + pub fn do_update_subnet(&mut self, caller: PrincipalId, payload: UpdateSubnetPayload) { + println!("{LOG_PREFIX}do_update_subnet: caller={caller}, payload={payload:?}"); + + let subnet_id = payload.subnet_id; + + // The engine controller canister is only allowed to mutate CloudEngine + // subnets, and only a small subset of fields. Other authorized callers + // (governance) can update any subnet and any field. + if caller == ENGINE_CONTROLLER_CANISTER_ID.get() { + let subnet_record = self.get_subnet_or_panic(subnet_id); + assert_eq!( + subnet_record.subnet_type, + i32::from(SubnetTypePb::CloudEngine), + "{LOG_PREFIX}do_update_subnet: engine controller may only update CloudEngine \ + subnets; subnet {subnet_id} has subnet_type {:?}", + subnet_record.subnet_type, + ); + ensure_engine_controller_payload_scope(&payload); + } self.validate_update_payload_chain_key_config(&payload); self.validate_update_sev_feature(&payload); - let subnet_id = payload.subnet_id; - let new_subnet_record = merge_subnet_record(self.get_subnet_or_panic(subnet_id), payload.clone()); @@ -213,6 +233,109 @@ impl Registry { } } +/// Defence-in-depth check that the engine controller canister never reaches +/// `do_update_subnet` with anything other than the small set of fields it is +/// allowed to manage (currently `subnet_admins` and `is_halted`). The engine +/// controller proxy already enforces this, but mirroring the check here keeps +/// the registry's invariants self-contained and prevents future drift if the +/// proxy's surface ever changes. +/// +/// Uses exhaustive destructuring so adding a new field to `UpdateSubnetPayload` +/// will fail to compile here until it is explicitly classified as +/// allowed-or-disallowed. +fn ensure_engine_controller_payload_scope(payload: &UpdateSubnetPayload) { + let UpdateSubnetPayload { + subnet_id: _, + // The fields the engine controller is allowed to set. + subnet_admins: _, + is_halted: _, + + max_ingress_bytes_per_message, + max_ingress_bytes_per_block, + max_ingress_messages_per_block, + max_block_payload_size, + unit_delay_millis, + initial_notary_delay_millis, + dkg_interval_length, + dkg_dealings_per_block, + start_as_nns, + subnet_type, + halt_at_cup_height, + features, + resource_limits, + chain_key_config, + chain_key_signing_enable, + chain_key_signing_disable, + max_number_of_canisters, + ssh_readonly_access, + ssh_backup_access, + max_artifact_streams_per_peer, + max_chunk_wait_ms, + max_duplicity, + max_chunk_size, + receive_check_cache_size, + pfn_evaluation_period_ms, + registry_poll_period_ms, + retransmission_request_ms, + set_gossip_config_to_default, + } = payload; + + let mut disallowed: Vec<&'static str> = vec![]; + macro_rules! check_none { + ($field:expr, $name:literal) => { + if $field.is_some() { + disallowed.push($name); + } + }; + } + check_none!( + max_ingress_bytes_per_message, + "max_ingress_bytes_per_message" + ); + check_none!(max_ingress_bytes_per_block, "max_ingress_bytes_per_block"); + check_none!( + max_ingress_messages_per_block, + "max_ingress_messages_per_block" + ); + check_none!(max_block_payload_size, "max_block_payload_size"); + check_none!(unit_delay_millis, "unit_delay_millis"); + check_none!(initial_notary_delay_millis, "initial_notary_delay_millis"); + check_none!(dkg_interval_length, "dkg_interval_length"); + check_none!(dkg_dealings_per_block, "dkg_dealings_per_block"); + check_none!(start_as_nns, "start_as_nns"); + check_none!(subnet_type, "subnet_type"); + check_none!(halt_at_cup_height, "halt_at_cup_height"); + check_none!(features, "features"); + check_none!(resource_limits, "resource_limits"); + check_none!(chain_key_config, "chain_key_config"); + check_none!(chain_key_signing_enable, "chain_key_signing_enable"); + check_none!(chain_key_signing_disable, "chain_key_signing_disable"); + check_none!(max_number_of_canisters, "max_number_of_canisters"); + check_none!(ssh_readonly_access, "ssh_readonly_access"); + check_none!(ssh_backup_access, "ssh_backup_access"); + check_none!( + max_artifact_streams_per_peer, + "max_artifact_streams_per_peer" + ); + check_none!(max_chunk_wait_ms, "max_chunk_wait_ms"); + check_none!(max_duplicity, "max_duplicity"); + check_none!(max_chunk_size, "max_chunk_size"); + check_none!(receive_check_cache_size, "receive_check_cache_size"); + check_none!(pfn_evaluation_period_ms, "pfn_evaluation_period_ms"); + check_none!(registry_poll_period_ms, "registry_poll_period_ms"); + check_none!(retransmission_request_ms, "retransmission_request_ms"); + if *set_gossip_config_to_default { + disallowed.push("set_gossip_config_to_default"); + } + + assert!( + disallowed.is_empty(), + "{LOG_PREFIX}do_update_subnet: engine controller may only update \ + `subnet_admins` and `is_halted`, but the following fields were also \ + set: {disallowed:?}", + ); +} + /// The payload of a proposal to update an existing subnet's configuration. /// /// See /rs/protobuf/def/registry/subnet/v1/subnet.proto @@ -540,6 +663,7 @@ mod tests { EcdsaCurve, EcdsaKeyId, SchnorrAlgorithm, SchnorrKeyId, VetKdCurve, VetKdKeyId, }; use ic_nervous_system_common_test_keys::{TEST_USER1_PRINCIPAL, TEST_USER2_PRINCIPAL}; + use ic_nns_constants::GOVERNANCE_CANISTER_ID; use ic_protobuf::registry::subnet::v1::{ CanisterCyclesCostSchedule, ChainKeyConfig as ChainKeyConfigPb, KeyConfig as KeyConfigPb, SubnetRecord as SubnetRecordPb, @@ -935,7 +1059,7 @@ mod tests { payload.chain_key_signing_enable = Some(vec![MasterPublicKeyId::Ecdsa(key)]); // Should panic because we are trying to enable a key that hasn't previously held it - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } #[test] @@ -990,7 +1114,7 @@ mod tests { payload.chain_key_signing_enable = Some(vec![MasterPublicKeyId::Ecdsa(key)]); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } #[test] @@ -1080,7 +1204,7 @@ mod tests { max_parallel_pre_signature_transcripts_in_creation: None, }); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } /// Returns an invariant-compliant Registry instance and an ID of a subnet @@ -1158,7 +1282,7 @@ mod tests { sev_enabled: Some(true), }); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } #[test] @@ -1175,7 +1299,7 @@ mod tests { sev_enabled: Some(false), }); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } /// Regression test: a payload with `features = Some(_)` but @@ -1195,7 +1319,7 @@ mod tests { sev_enabled: None, }); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } #[test] @@ -1210,7 +1334,7 @@ mod tests { sev_enabled: Some(true), }); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); let subnet_features = registry .get_subnet_or_panic(subnet_id) @@ -1233,7 +1357,7 @@ mod tests { }); // Should not panic because we are not changing SEV-related subnet features. - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } #[test] @@ -1244,7 +1368,7 @@ mod tests { { let mut payload = make_empty_update_payload(subnet_id); payload.features = None; - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } // Enable non-SEV-related features that can be enabled after the subnet was created. @@ -1255,7 +1379,7 @@ mod tests { http_requests: true, sev_enabled: None, }); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } } @@ -1322,7 +1446,7 @@ mod tests { payload.chain_key_signing_enable = Some(vec![master_public_key_held_by_subnet.clone()]); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); // Make sure it's actually in the list of enabled chain keys. assert!( @@ -1351,7 +1475,7 @@ mod tests { payload.chain_key_signing_disable = Some(vec![master_public_key_held_by_subnet.clone()]); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); // Ensure it's now removed from list of enabled subnets. assert!( @@ -1438,7 +1562,7 @@ mod tests { payload.chain_key_signing_disable = Some(vec![MasterPublicKeyId::Ecdsa(key.clone())]); // Should panic because we are trying to enable/disable same key - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } #[test] @@ -1509,7 +1633,7 @@ mod tests { ..make_empty_update_payload(subnet_id) }; - registry.do_update_subnet(payload.clone()); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload.clone()); // Try to update the subnet by adding a new key and removing one of the existing keys let payload = UpdateSubnetPayload { @@ -1521,7 +1645,7 @@ mod tests { }; // Should panic because we are trying to delete an existing key - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } #[test] @@ -1541,7 +1665,7 @@ mod tests { None, ); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } #[test] @@ -1561,7 +1685,7 @@ mod tests { Some(99), ); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } fn update_subnet_payload_with_key_config( @@ -1622,7 +1746,7 @@ mod tests { let mut payload = make_empty_update_payload(subnet_id); payload.subnet_admins = Some(vec![user1, user2]); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); assert_eq!( registry.get_subnet_or_panic(subnet_id).subnet_admins, @@ -1639,7 +1763,7 @@ mod tests { // First set an admin. let mut payload = make_empty_update_payload(subnet_id); payload.subnet_admins = Some(vec![user1]); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); assert_eq!( registry.get_subnet_or_panic(subnet_id).subnet_admins, vec![PrincipalIdPb::from(user1)], @@ -1648,7 +1772,7 @@ mod tests { // Then clear it via Some(vec![]). let mut payload = make_empty_update_payload(subnet_id); payload.subnet_admins = Some(vec![]); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); assert_eq!( registry.get_subnet_or_panic(subnet_id).subnet_admins, Vec::::new(), @@ -1663,11 +1787,11 @@ mod tests { let mut payload = make_empty_update_payload(subnet_id); payload.subnet_admins = Some(vec![user1]); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); // A subsequent update with `subnet_admins: None` must not change the list. let payload = make_empty_update_payload(subnet_id); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); assert_eq!( registry.get_subnet_or_panic(subnet_id).subnet_admins, @@ -1688,7 +1812,7 @@ mod tests { let mut payload = make_empty_update_payload(subnet_id); payload.subnet_admins = Some(admins); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); } #[test] @@ -1703,6 +1827,96 @@ mod tests { let mut payload = make_empty_update_payload(subnet_id); payload.subnet_admins = Some(vec![*TEST_USER1_PRINCIPAL]); - registry.do_update_subnet(payload); + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); + } + + /// Builds a registry that already has a `CloudEngine` subnet, suitable + /// for exercising the engine-controller permission checks on + /// `do_update_subnet`. + fn make_registry_with_cloud_engine_subnet() -> (Registry, SubnetId) { + use crate::common::test_helpers::prepare_registry_with_cloud_engine_subnet; + + let mut registry = invariant_compliant_registry(0); + let (mutate_request, subnet_id) = prepare_registry_with_cloud_engine_subnet(1, 2); + registry.maybe_apply_mutation_internal(mutate_request.mutations); + (registry, subnet_id) + } + + #[test] + fn engine_controller_can_update_cloud_engine_subnet_admins() { + use ic_nns_constants::ENGINE_CONTROLLER_CANISTER_ID; + + let (mut registry, subnet_id) = make_registry_with_cloud_engine_subnet(); + + let new_admins = vec![*TEST_USER1_PRINCIPAL, *TEST_USER2_PRINCIPAL]; + let mut payload = make_empty_update_payload(subnet_id); + payload.subnet_admins = Some(new_admins.clone()); + + registry.do_update_subnet(ENGINE_CONTROLLER_CANISTER_ID.get(), payload); + + let subnet_record = registry.get_subnet_or_panic(subnet_id); + let stored: Vec = subnet_record + .subnet_admins + .into_iter() + .map(|p| PrincipalId::try_from(p.raw.as_slice()).unwrap()) + .collect(); + assert_eq!(stored, new_admins); + } + + #[test] + #[should_panic(expected = "engine controller may only update CloudEngine subnets")] + fn engine_controller_cannot_update_non_cloud_engine_subnet() { + use ic_nns_constants::ENGINE_CONTROLLER_CANISTER_ID; + + // Default fixture is an Application subnet. + let (mut registry, subnet_id) = make_registry_for_update_subnet_tests(); + + let mut payload = make_empty_update_payload(subnet_id); + payload.subnet_admins = Some(vec![*TEST_USER1_PRINCIPAL]); + + registry.do_update_subnet(ENGINE_CONTROLLER_CANISTER_ID.get(), payload); + } + + #[test] + #[should_panic(expected = "engine controller may only update `subnet_admins` and `is_halted`")] + fn engine_controller_cannot_update_disallowed_fields() { + use ic_nns_constants::ENGINE_CONTROLLER_CANISTER_ID; + + let (mut registry, subnet_id) = make_registry_with_cloud_engine_subnet(); + + let mut payload = make_empty_update_payload(subnet_id); + // `max_number_of_canisters` is outside the engine controller's scope. + payload.max_number_of_canisters = Some(123); + + registry.do_update_subnet(ENGINE_CONTROLLER_CANISTER_ID.get(), payload); + } + + #[test] + fn engine_controller_can_set_is_halted() { + use ic_nns_constants::ENGINE_CONTROLLER_CANISTER_ID; + + let (mut registry, subnet_id) = make_registry_with_cloud_engine_subnet(); + + let mut payload = make_empty_update_payload(subnet_id); + payload.is_halted = Some(true); + + registry.do_update_subnet(ENGINE_CONTROLLER_CANISTER_ID.get(), payload); + + assert!(registry.get_subnet_or_panic(subnet_id).is_halted); + } + + #[test] + fn governance_is_not_restricted_to_cloud_engine_subnets() { + // Sanity check: governance must still be able to update non-CloudEngine + // subnets (it goes through the same code path now). + let (mut registry, subnet_id) = make_registry_for_update_subnet_tests(); + + let mut payload = make_empty_update_payload(subnet_id); + payload.max_number_of_canisters = Some(123); + + registry.do_update_subnet(GOVERNANCE_CANISTER_ID.get(), payload); + + let subnet_record = registry.get_subnet_or_panic(subnet_id); + assert_eq!(subnet_record.max_number_of_canisters, 123); } } diff --git a/rs/registry/canister/tests/common/test_helpers.rs b/rs/registry/canister/tests/common/test_helpers.rs index c3d41cb2d474..f63ad19464b5 100644 --- a/rs/registry/canister/tests/common/test_helpers.rs +++ b/rs/registry/canister/tests/common/test_helpers.rs @@ -213,6 +213,11 @@ pub fn prepare_registry_with_nodes_from_template( /// subnets). The returned request is meant to be pushed as an additional init /// mutate request on top of the invariant-compliant base; it also returns the id /// of the new CloudEngine subnet. +/// +/// NOTE: A unit-test-local copy of this helper with the same name and +/// signature lives at `src/common/test_helpers.rs` because the latter is +/// `#[cfg(test)]` and not accessible from integration tests. Keep them in +/// sync. pub fn prepare_registry_with_cloud_engine_subnet( node_count: u64, starting_mutation_id: u8, diff --git a/rs/registry/canister/unreleased_changelog.md b/rs/registry/canister/unreleased_changelog.md index 93c14851bbe2..b74d0b6fdf37 100644 --- a/rs/registry/canister/unreleased_changelog.md +++ b/rs/registry/canister/unreleased_changelog.md @@ -30,6 +30,13 @@ on the process that this file is part of, see * The `create_subnet` and `delete_subnet` endpoints can now be called by the engine controller canister (`si2b5-pyaaa-aaaaa-aaaja-cai`) in addition to the governance canister. +* The `update_subnet` and `deploy_guestos_to_all_subnet_nodes` endpoints can now + also be called by the engine controller canister + (`si2b5-pyaaa-aaaaa-aaaja-cai`) in addition to the governance canister. When + invoked by the engine controller, both endpoints are restricted to acting on + `CloudEngine` subnets only — any attempt to target a subnet of a different + type is rejected. Calls from the governance canister are unaffected and may + still target subnets of any type. * **SEV on existing subnets:** Reverted — `sev_enabled` can once again only be set at subnet creation; any update_subnet proposal that would change the effective `sev_enabled` value (in either direction, including via wholesale `features` replacement with `sev_enabled` left unset) is rejected. From a60aa51b068389cd8d237bf7a954f305fc64b16d Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Fri, 12 Jun 2026 15:48:36 +0200 Subject: [PATCH 25/75] feat: add IDX skills (#10454) This introduces a skill to fix determinism issues, as well as two other skills that can be used by agents and humans alike to perform IC builds on non-DFINITY infrastructure. --- .../build-without-dfinity-infra/SKILL.md | 73 +++++++ .claude/skills/fix-build-determinism/SKILL.md | 201 ++++++++++++++++++ .claude/skills/run-in-dev-container/SKILL.md | 53 +++++ 3 files changed, 327 insertions(+) create mode 100644 .claude/skills/build-without-dfinity-infra/SKILL.md create mode 100644 .claude/skills/fix-build-determinism/SKILL.md create mode 100644 .claude/skills/run-in-dev-container/SKILL.md diff --git a/.claude/skills/build-without-dfinity-infra/SKILL.md b/.claude/skills/build-without-dfinity-infra/SKILL.md new file mode 100644 index 000000000000..6e4a4df8f58e --- /dev/null +++ b/.claude/skills/build-without-dfinity-infra/SKILL.md @@ -0,0 +1,73 @@ +--- +name: build-without-dfinity-infra +description: Use when you need to run a Bazel build or test outside DFINITY's internal infrastructure — i.e. without access to the internal remote cache / remote downloader (bazel-remote.idx.dfinity.network), e.g. on a personal machine, in a sandbox, or for a reproducibility check. Two ways: --config=local, or bypassing the workspace bazelrc. +--- + +# Building without DFINITY's internal infrastructure + +The repo's `.bazelrc` imports `bazel/conf/.bazelrc.internal`, which points Bazel +at DFINITY's internal remote cache and remote downloader +(`bazel-remote.idx.dfinity.network`). These endpoints are reachable only from +inside DFINITY's internal network — in practice that essentially means a +**devenv** machine. They are **not** available on, e.g., a namespace.so devbox, +a sandbox, or CI without those credentials, where a plain `bazel build` will fail +or stall. + +If you're unsure whether the infra is reachable from where you are, probe it: + +```sh +curl -sS --max-time 5 -o /dev/null https://bazel-remote.idx.dfinity.network \ + && echo "internal cache reachable" \ + || echo "internal cache NOT reachable — build with --config=local" +``` + +When it's not reachable, build with one of the two approaches below. + +## Option 1 — `--config=local` (recommended) + +Keeps the full workspace configuration but empties `--remote_cache=` and +`--experimental_remote_downloader=` (see the `build:local` lines in +`bazel/conf/.bazelrc.internal`), so nothing contacts the internal endpoints: + +```sh +bazel build --config=local //my:target +``` + +Use this when you want the normal build config minus the remote cache. It is also +what you want for reproducibility checks, where a cache hit could otherwise mask +non-determinism. + +## Option 2 — bypass the workspace bazelrc entirely + +Ignore the workspace `.bazelrc` (which is what pulls in `.bazelrc.internal`) and +load only the minimal build config, via *startup* options: + +```sh +bazel --noworkspace_rc --bazelrc=bazel/conf/.bazelrc.build build //my:target +``` + +These are startup options (before the `build` subcommand), and `--bazelrc` is +resolved relative to the current working directory. Use this when you want +nothing from the workspace/internal config at all — only the settings required to +build. + +## Which to use + +Prefer `--config=local`: it's a single build flag and keeps the rest of the +workspace config intact. Reach for the `--noworkspace_rc` form only when you +specifically need to exclude everything the workspace `.bazelrc` imports. + +## Running in the dev container + +This is orthogonal to *where* you build — still run these through the pinned dev +container. See the **run-in-dev-container** skill for how to invoke +`container-run.sh`, including the podman/docker runtime choice for hosts without +podman. + +## Caveat: system tests won't work + +These flags only drop the remote cache/downloader; they can't replace the rest of +the internal infrastructure. In particular, **system tests will not run**: they +provision VMs via Farm, which lives inside DFINITY's internal network and is +unreachable from outside it. `--config=local` removes the cache but cannot +substitute Farm. Limit yourself to `bazel build` and non-system tests. diff --git a/.claude/skills/fix-build-determinism/SKILL.md b/.claude/skills/fix-build-determinism/SKILL.md new file mode 100644 index 000000000000..136a7de28dab --- /dev/null +++ b/.claude/skills/fix-build-determinism/SKILL.md @@ -0,0 +1,201 @@ +--- +name: fix-build-determinism +description: Use this when asked to fix a Bazel build reproducibility / determinism issue — a target whose outputs differ between builds (e.g. across machines, users, or checkout locations), typically because something bakes an absolute build path or a timestamp into an artifact. +--- + +# Fix build determinism issues + +A reproducible build produces byte-identical outputs regardless of *where* it +runs (which directory it's checked out in, which output base, which user). The +IC publishes reproducible artifacts, so any target that bakes a build-time +absolute path, timestamp, or other environment detail into its output is a bug. + +The usual culprits are externally-built dependencies — `http_archive`s built +with `rules_foreign_cc` (autotools/cmake) and Rust crates with `build.rs` — +because they escape Bazel's normal path/timestamp scrubbing and can embed +`$PWD`, an install `--prefix`, `__DATE__`/`__TIME__`, a build-script probe +artifact, etc. into their outputs. + +All commands run from the repository root (`cd "$(git rev-parse --show-toplevel)"`). + +## The `hunt` script + +Diagnosis is driven by the upstream `hunt` reproducibility script. It is **not +checked into this repo** — get it from [its gist](https://gist.github.com/nmattia/dc8a1d4f3bc36c9c0133d15f06acc74e), save it +at the repo root as `hunt`, and `chmod +x` it. It builds a target **twice**, each +time in a *freshly cloned* checkout under an output base nested at a **different +depth** — so the absolute build path differs between the two runs the same way +it would differ between two machines. It writes each build's +`--execution_log_json_file` and diffs the `actualOutputs` (path + content +digest) of every action. Any output whose digest differs between the two runs is +a non-reproducible artifact. A clean run ends with `builds 1 - 2: no diff ✓`. + +``` +usage: ./hunt [--root ROOT] [--startup-options OPTS] [--build-options OPTS] [--runs N] TARGET + + --root ROOT dir for the per-run checkouts, output bases and execlogs. + Default: a fresh `mktemp -d`. + --startup-options OPTS extra bazel *startup* options, one space-separated string. + --build-options OPTS extra bazel *build* options, one space-separated string. + --runs N number of builds to compare (default: 2). + TARGET label to build, e.g. //:mkfs.ext4 +``` + +### Always pass `--build-options='--config=local'` + +Run with `--build-options='--config=local'`, which builds **without the internal +remote cache** — essential, because a cache hit would serve a previously-built +(possibly non-reproducible) artifact and *mask* the very non-determinism you're +hunting. See the **build-without-dfinity-infra** skill for what `--config=local` +does. (`--startup-options`/`--build-options` are how you feed bazel any other +flags the build needs.) + +### Running it + +Run `hunt` inside the dev container so the build environment is the pinned one. +See the **run-in-dev-container** skill for how to invoke `container-run.sh` +(including on hosts without podman): + +```sh +# quick pass/fail (artifacts land in an ephemeral in-container tempdir): +./ci/container/container-run.sh ./hunt --build-options='--config=local' //my:target + +# to *diagnose* (step 3 needs the two builds' outputs to survive the container), +# point --root at a bind-mounted path under /ic so the artifacts persist on the host: +./ci/container/container-run.sh ./hunt --root /ic/,hunt --build-options='--config=local' //my:target +``` + +Gotchas: +- `hunt` does `git clone` of the repo, so **your fix must be committed** (to the + current branch) for a hunt run to pick it up. Iterate: commit → hunt → repeat. +- With the default tempdir, the checkouts/output bases live in the container's + `/tmp` and vanish when the container exits — fine for a verdict, but use + `--root` under `/ic` when you need to inspect artifacts. +- Bazel marks its output trees read-only. To clean the hunt root between runs: + `chmod -R u+w && rm -rf `. +- Point `--root` at a path on the same filesystem as the repo for faster + (hardlinked) clones. + +## Procedure + +1. **Run `hunt --build-options='--config=local'` on the failing target** and read + the mismatch JSON it prints — a list of `{path, digest}` for outputs that differed. + +2. **Look at the first differing target/output.** Non-determinism cascades: one + non-reproducible artifact (a generated source, a static lib, a tool binary) + makes everything that embeds it differ too. Fix the *earliest / most upstream* + differing artifact first; re-running often makes the downstream diffs vanish. + Map the output path back to the dependency that produces it (e.g. + `external/+_repo_rules+/...` → the `http_archive` named ``; + `external/.../-/...` → a crate). + +3. **Pin down *what* differs.** The execlog only gives digests. Pull the two + actual artifacts from the persisted output bases (`$ROOT/output-base-1/...` + vs `$ROOT/output-base-2/...` — use `--root` so these survive) and compare them + directly: + ```sh + cmp "$A" "$B" # confirm they differ + diff <(strings -a "$A"|sort -u) <(strings -a "$B"|sort -u) # build paths / dates + diff <(readelf -SW "$A") <(readelf -SW "$B") # for ELF: which section + ``` + Typical findings: an embedded absolute build path (`/.../sandbox/.../...`), a + `__DATE__`/`__TIME__` string, a build-script probe artifact, or archive + ordering. (Bazel's C toolchain already redacts `__DATE__`/`__TIME__` and + passes `-no-canonical-prefixes`, so it's almost always an embedded path.) + + From the offending string, settle on a **one-artifact probe** for the bad + pattern — a command that's non-empty on a broken build and empty once fixed, + so you can judge a *single* build without re-running the full hunt. Pick the + most specific stable marker the leak leaves behind (the `sandbox` path + component, the exec-root / output-base prefix, `.build_tmpdir`, a date, the + stray probe filename), e.g.: + ```sh + strings -a "$A" | grep -n sandbox # expect matches now; none once fixed + ``` + +4. **Read the dependency's source** to find where that string comes from — a + `configure`-substituted install path, a `build.rs` writing `env!("OUT_DIR")` + or a `canonicalize()`d path into generated code, a hardcoded `PREFIX/...` + constant, a stray probe file left in `OUT_DIR`, etc. + +5. **Check out the source locally and iterate against it.** Get the dependency at + the exact version (same `urls`/`sha256` as in `MODULE.bazel`, or the crate + source), then point Bazel at your local copy so you can edit and rebuild + without re-uploading a patch each time: + ```sh + bazel build --config=local --override_repository==/abs/path/to/src //my:target + ``` + (Find `` with `bazel query --output=build ` or from the + execlog path.) Edit, rebuild, and run the step-3 probe on the freshly-built + artifact as a fast inner-loop check — when it comes back empty the embedded + dependency is gone: + ```sh + strings -a bazel-bin/.../ | grep sandbox || echo clean + ``` + This single-build probe is a quick check only; it does **not** replace the + full reproducibility confirmation in step 8. Keep a pristine copy to `diff` + against for the patch. + +6. **Report it upstream.** A local patch is a workaround — the real fix belongs + in the dependency, and an upstream fix lets us eventually drop the patch. File + an issue on the dependency's tracker (for a crate or other GitHub project, + `https://github.com///issues/new`), including: + - a short explanation of the bug and its impact, e.g. *"`build.rs` writes the + absolute `$OUT_DIR` path into the generated `foo.rs`, so the crate's rlib + differs between builds at different filesystem locations, breaking + reproducible/hermetic builds."* + - a minimal reproducible example if the maintainer will likely need one — the + smallest snippet that emits the offending output, or simply "build at two + different paths and `diff` the artifacts" (the step-3 probe doubles as the + symptom). + + Keep the resulting issue/PR URL; reference it from the patch header below. + +7. **Capture the fix as a patch** applied at fetch time (don't fork the dep): + + - **`http_archive` dependency** — add a patch (convention: + `third_party/_.patch`) and reference it from the archive in + `MODULE.bazel`: + ```python + http_archive( + name = "", + ... + patches = ["//third_party:_.patch"], + patch_strip = 1, + ) + ``` + + - **Rust crate** — add a patch under `bazel/` and a crate annotation in + `bazel/rust.MODULE.bazel`: + ```python + crate.annotation( + crate = "", + patch_args = ["-p1"], + patches = ["@@//bazel:.patch"], + ) + ``` + + The patch is a `git diff` (paths `a/…` `b/…`); leading `#` comment lines + describing the fix — and linking the upstream ticket from step 6 — are fine + and conventional here. Prefer the smallest patch + that removes the environment dependency. If the offending artifact is + something nothing downstream consumes (an extra tool the dependency + builds/installs), it's also valid to just stop shipping it (e.g. trim it in a + `rules_foreign_cc` `postfix_script`) rather than make it reproducible. + +8. **Re-run `hunt` to confirm** `builds 1 - 2: no diff ✓`. If a *new* (further + downstream) difference appears, repeat from step 2 — that's the cascade + resolving one layer at a time. Verify the real consumers of the target still + build. + +## Worked examples + +| Dependency | Kind | Cause | Fix | Commit | +| --- | --- | --- | --- | --- | +| **askama** (`bazel/askama.patch`) | crate (derive macro) | `Path::canonicalize()` resolved the sandbox symlink to the repo's real path; the resulting relative path's `..`-count depended on the sandbox path *depth*, so the rlib changed across output-base/nest depths. | Skip `canonicalize()` so both paths stay anchored to the sandbox. | `0d9c593299` (#10167) | +| **rustix** (`bazel/rustix.patch`) | crate (`build.rs`) | The `can_compile()` probe left a `rustix_test_can_compile` artifact in `OUT_DIR`, which rules_rust captures as a cacheable output; it embeds non-deterministic compiler-internal metadata. | Delete the probe artifact after use (upstream PR 1628). | `9f0476004b` (#10353) | +| **libssh2-sys** (`bazel/libssh2-sys.patch`) | crate (`build.rs`) | `build.rs` generated a pkgconfig file containing absolute build paths. | Disable that generation. | `737666659c` (#3197) | +| **e2fsprogs / mke2fs** (`third_party/e2fsprogs_no_external_config.patch`) | `http_archive` (rules_foreign_cc) | `configure`'s `--prefix` is the per-build `$BUILD_TMPDIR`, so the compiled-in `mke2fs.conf` path (`ROOT_SYSCONFDIR`) landed in the binary's `.rodata`. | Empty `config_fn` (use the built-in default profile) + trim the unused, also-non-reproducible extra tools from the install tree. | `cf7fe5147b` | + +The askama case is the canonical illustration of *why* `hunt` varies the nest +depth: the bug only manifests when the build path's depth changes between runs. diff --git a/.claude/skills/run-in-dev-container/SKILL.md b/.claude/skills/run-in-dev-container/SKILL.md new file mode 100644 index 000000000000..dbfe24c9c4a5 --- /dev/null +++ b/.claude/skills/run-in-dev-container/SKILL.md @@ -0,0 +1,53 @@ +--- +name: run-in-dev-container +description: Use when you need to run a command (build, test, tool) inside the IC dev container via ./ci/container/container-run.sh — including on a host that has Docker but not podman (set CONTAINER_RUNTIME=docker). +--- + +# Running commands in the IC dev container + +`./ci/container/container-run.sh` runs a command inside the pinned IC dev +container (the `ghcr.io/dfinity/ic-dev` image), bind-mounting the repo checkout +at `/ic` and reusing `~/.cache` for the Bazel/cargo/zig caches. + +Prefer running builds and build tooling through it: it gives you the exact, +pinned toolchain environment, and standardizing on the container — regardless of +whether it's backed by podman or docker — keeps things simple and consistent. + +## Choosing the container runtime + +The script supports two runtimes, selected by the `CONTAINER_RUNTIME` env var: + +- **podman** (default) — rootful and privileged. +- **docker** — for hosts that have the Docker daemon but **no podman**. + +Which one to use depends on where you are: + +- **namespace.so devboxes** (e.g. this machine — `test -d /.namespace`): use + **docker**. podman isn't available there, and the namespace.so daemon can't do + some things podman's setup expects (e.g. bind-mounting the host's `/tmp`). +- **DFINITY infra, in particular a "devenv" machine** (which `container-run.sh` + detects via `/var/lib/cloud/instance` plus a `/hoststorage` mount): use + **podman** — the default, so no env var needed. + +On a docker host, prefix every invocation with `CONTAINER_RUNTIME=docker`: + +```sh +# interactive shell in the container +CONTAINER_RUNTIME=docker ./ci/container/container-run.sh + +# run a single command and exit +CONTAINER_RUNTIME=docker ./ci/container/container-run.sh [args...] +``` + +If `CONTAINER_RUNTIME` is unsupported the script errors out early; if the chosen +runtime's daemon isn't reachable it prints which command it tried. + +## Notes + +- The repo is mounted at `/ic` and that's the working directory, so invoke + repo-local scripts with a relative path, e.g. + `CONTAINER_RUNTIME=docker ./ci/container/container-run.sh ./path/to/script.sh`. +- The image is pulled from `ghcr.io` on first use (large, one-time). +- Anything the command writes under `/ic` (or `~/.cache`) persists on the host, + since those are bind-mounted. +- Don't nest: the script refuses to run inside an existing container. From bff673d874261e597482cb531084372ba76814f4 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Fri, 12 Jun 2026 13:52:48 -0700 Subject: [PATCH 26/75] feat: [NODE-1963] Copy only the latest few logfiles (#10425) In theory, at this point, we have already collected all of the logs we need. Copying the logs here saves the last few logs before a shutdown, so a handful of the most recent logfiles should be fine. --- .../guestos/init/setup-encryption/setup-var-encryption.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ic-os/components/guestos/init/setup-encryption/setup-var-encryption.sh b/ic-os/components/guestos/init/setup-encryption/setup-var-encryption.sh index 64d4f6b3db76..b40274766c5a 100755 --- a/ic-os/components/guestos/init/setup-encryption/setup-var-encryption.sh +++ b/ic-os/components/guestos/init/setup-encryption/setup-var-encryption.sh @@ -9,9 +9,9 @@ function transfer_log_state() { MACHINE_ID=$(cat /etc/machine-id || echo invalid) echo "Successfully mounted old /var partition, copying contents for machine id: ${MACHINE_ID}" - # First, copy actual journal files. - mkdir -p /mnt/var_new/log/journal - if cp -vr /mnt/var_old/log/journal/"${MACHINE_ID}" /mnt/var_new/log/journal/"${MACHINE_ID}"; then + # First, copy latest journal files. + mkdir -p /mnt/var_new/log/journal/"${MACHINE_ID}" + if cp -pv $(ls -t /mnt/var_old/log/journal/"${MACHINE_ID}"/*.journal | head -3) /mnt/var_new/log/journal/"${MACHINE_ID}"/; then chown -R root.systemd-journal /mnt/var_new/log/journal/ chcon -R system_u:object_r:systemd_journal_t:s0 /mnt/var_new/log/journal/"${MACHINE_ID}" ls -lZ /mnt/var_new/log/journal/"${MACHINE_ID}" From de83a2b0bb1349f2f51a9b183856b5c33c6a1cd4 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Fri, 12 Jun 2026 23:54:49 -0700 Subject: [PATCH 27/75] fix: Fixup bare metal deployment configs (#10450) --- ic-os/dev-tools/bare_metal_deployment/dm1.yaml | 6 +++--- ic-os/dev-tools/bare_metal_deployment/zh2.yaml | 8 -------- 2 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 ic-os/dev-tools/bare_metal_deployment/zh2.yaml diff --git a/ic-os/dev-tools/bare_metal_deployment/dm1.yaml b/ic-os/dev-tools/bare_metal_deployment/dm1.yaml index e3eaf87f9e55..d167fc423f96 100644 --- a/ic-os/dev-tools/bare_metal_deployment/dm1.yaml +++ b/ic-os/dev-tools/bare_metal_deployment/dm1.yaml @@ -2,7 +2,7 @@ file_share_url: 10.12.7.240 file_share_dir: /srv/images file_share_image_filename: setupos.bmd.img inject_image_node_reward_type: type3.1 -inject_image_ipv6_prefix: 2602:fb2b:100:14 -inject_image_ipv6_gateway: 2602:fb2b:100:14::1 +inject_image_ipv6_prefix: 2602:fb2b:100:10 +inject_image_ipv6_gateway: 2602:fb2b:100:10::1 inject_image_verbose: false -inject_enable_trusted_execution_environment: false \ No newline at end of file +inject_enable_trusted_execution_environment: false diff --git a/ic-os/dev-tools/bare_metal_deployment/zh2.yaml b/ic-os/dev-tools/bare_metal_deployment/zh2.yaml deleted file mode 100644 index 5e7ee1b6bd82..000000000000 --- a/ic-os/dev-tools/bare_metal_deployment/zh2.yaml +++ /dev/null @@ -1,8 +0,0 @@ -file_share_url: 10.10.101.234 -file_share_dir: /srv/images -file_share_image_filename: setupos.bmd.img -inject_image_node_reward_type: type3.1 -inject_image_ipv6_prefix: 2a00:fb01:400:44 -inject_image_ipv6_gateway: 2a00:fb01:400:44::1 -inject_image_verbose: false -inject_enable_trusted_execution_environment: false \ No newline at end of file From 8e12eec3053899fa0a8bfc98d6746493325b1e42 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Sat, 13 Jun 2026 21:24:59 +0200 Subject: [PATCH 28/75] fix: evict dead pooled XNet connections via HTTP/2 keep-alive pings (#10455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Since the migration of the XNet payload builder to HTTP/2 (#1506), a dead or stalled pooled connection to an XNet endpoint is reused **indefinitely**: - Cancelling a request (e.g. due to the 5-second query timeout in `XNetClientImpl::query`) only resets the respective h2 *stream*; it does not affect the underlying connection. - The connection pool only drops connections that report themselves as closed; a stalled-but-established h2 connection never does. A connection that ends up dead/stalled (e.g. due to packet loss or CPU starvation while under load) therefore black-holes **all** pulls from the respective subnet pair until the replica restarts. Under the previous HTTP/1.1 client this could not happen, because cancelling a request closes its connection. ### Observed impact `//rs/tests/message_routing/xnet:xnet_slo_120_subnets_staging_test` failed reproducibly: the synchronized startup of 120×119 TLS connections (while replicas were CPU-starved) wedged 191 directed subnet pairs. Each wedged pair timed out on every pull (at the 5s timeout, ~2.3/s) for the entire run with zero recovery, pushing best-effort calls on 20 subnets past their 30s deadline (5.7%–13.7% failed calls vs. the 5% threshold). Per-subnet error rate was predicted by the number of wedged pairs involving that subnet with correlation 0.997. ## Fix Enable HTTP/2 keep-alive pings on the XNet client: 10s interval, 5s timeout, also while idle (`pool_max_idle_per_host(1)` keeps an idle connection pooled for up to 600s). A dead connection is now detected and closed within ~15s — well below the 30s best-effort deadline — after which the next query establishes a fresh connection. Note: this requires supplying an h2 timer via `.timer(TokioTimer::new())` in addition to the existing `.pool_timer(...)`; without it hyper panics at runtime once keep-alive is enabled. As defense in depth, also set a 5s TCP connect timeout on the `HttpConnector` underlying `TlsConnector` (both `new` and `new_for_tests`), so a stuck handshake fails fast rather than relying solely on the per-query timeout. This does not address the observed failure on its own — the wedged connections had completed TCP+TLS and stalled at the h2 layer — but it bounds connection-setup stalls. Cost: at most ~119 connections per node → ~12 PING frames/s/node; both endpoints are replica-controlled (hyper's h2 server ACKs pings natively). ## Verification On CI the `Release System Tests` job [succeeds](https://github.com/dfinity/ic/actions/runs/27438248671/job/81105591015) including `//rs/tests/message_routing/xnet:xnet_slo_120_subnets_staging_test_colocate` ([BuildBuddy](https://dash.dm1-idx1.dfinity.network/invocation/2d53507f-fe7d-45a8-a006-1fecfd20666e?target=%2F%2Frs%2Ftests%2Fmessage_routing%2Fxnet%3Axnet_slo_120_subnets_staging_test_colocate&targetStatus=5)). Manual runs (from `dm1`) succeed as well: ``` bazel test //rs/tests/message_routing/xnet:xnet_slo_120_subnets_staging_test \ --cache_test_results=no --test_output=errors \ --test_arg=--set-required-host-features=dc=dm1 ``` - Before: FAILED — 20/120 subnets above the 5% failed-call threshold (5.7%–13.7%). - After: **PASSED in 745.8s — 0 failed calls on all 120 subnets** (worst subnet: 0/56049). - `//rs/xnet/payload_builder:payload_builder_test` and `:payload_builder_integration` pass (the latter exercises real client queries and would catch the missing-timer panic). After reducing the ping frequency to a 10s interval and adding the connect timeout (review follow-up), `bazel test //rs/tests/message_routing/xnet:xnet_slo_120_subnets_staging_test --cache_test_results=no` still succeeds, and the unit/integration tests above still pass. --- rs/xnet/hyper/src/lib.rs | 7 +++++++ rs/xnet/payload_builder/src/lib.rs | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/rs/xnet/hyper/src/lib.rs b/rs/xnet/hyper/src/lib.rs index 303ca2aa3861..5039709220ad 100644 --- a/rs/xnet/hyper/src/lib.rs +++ b/rs/xnet/hyper/src/lib.rs @@ -11,6 +11,7 @@ use std::{ pin::Pin, sync::Arc, task::{Context, Poll}, + time::Duration, }; use tokio::net::TcpStream; use tower::{BoxError, Service}; @@ -39,6 +40,9 @@ impl TlsConnector { pub fn new(tls: Arc) -> Self { let mut http = HttpConnector::new(); http.enforce_http(false); + // Fail fast on a stuck TCP connect instead of relying solely on the + // per-query timeout (defense in depth against connection-setup storms). + http.set_connect_timeout(Some(Duration::from_secs(5))); Self { connection_type: ConnectionType::Tls, http, @@ -51,6 +55,9 @@ impl TlsConnector { pub fn new_for_tests(tls: Arc) -> Self { let mut http = HttpConnector::new(); http.enforce_http(false); + // Fail fast on a stuck TCP connect instead of relying solely on the + // per-query timeout (defense in depth against connection-setup storms). + http.set_connect_timeout(Some(Duration::from_secs(5))); Self { connection_type: ConnectionType::Raw, http, diff --git a/rs/xnet/payload_builder/src/lib.rs b/rs/xnet/payload_builder/src/lib.rs index 7a97c91aabc7..652db63485fb 100644 --- a/rs/xnet/payload_builder/src/lib.rs +++ b/rs/xnet/payload_builder/src/lib.rs @@ -1754,8 +1754,8 @@ struct XNetClientImpl { } impl XNetClientImpl { - /// Creates a new `XNetClientImpl` with a request timeout of 1 second and at - /// most 1 idle connection per host. + /// Creates a new `XNetClientImpl` with a request timeout of 5 seconds, at + /// most 1 idle connection per host and HTTP/2 keep-alive pings. fn new( metrics_registry: &MetricsRegistry, tls: Arc, @@ -1767,9 +1767,24 @@ impl XNetClientImpl { let https = TlsConnector::new_for_tests(tls); // TODO(MR-28) Make timeout configurable. + // + // HTTP/2 keep-alive pings are necessary to evict dead pooled connections: + // cancelling a request (e.g. due to the 5-second query timeout) only resets + // the respective stream, it does not affect the underlying connection. And + // the pool only drops connections that report themselves as closed. So + // without keep-alive a connection that is dead or stalled (e.g. due to + // packet loss while under load) would be reused indefinitely, with every + // query against it timing out. With keep-alive, such a connection is closed + // within `interval + timeout` seconds and a fresh connection is established + // on the next query. let http_client: Client> = Client::builder(TokioExecutor::new()) .http2_only(true) + // Timer required by HTTP/2 keep-alive. + .timer(TokioTimer::new()) + .http2_keep_alive_interval(Some(Duration::from_secs(10))) + .http2_keep_alive_timeout(Duration::from_secs(5)) + .http2_keep_alive_while_idle(true) .pool_timer(TokioTimer::new()) .pool_idle_timeout(Some(Duration::from_secs(600))) .pool_max_idle_per_host(1) From f8f89b36c66690171679e490b399d0e22dd0e9d4 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn <99166915+eichhorl@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:48:51 +0200 Subject: [PATCH 29/75] chore(P2P): Only increment error metric if transport infos of all registry versions are empty (#10408) The peer manager maintains connections to all peers that are in the subnet record in any registry version between the "oldest version in use" and the latest locally available registry version. The "oldest registry version in use" is determined by the CUP. In case of a subnet creation, this CUP is a registry CUP that contains the registry version just _before_ the subnet was created (see [here](https://sourcegraph.com/r/github.com/dfinity/ic@8cf338f5ef95432dc61233df889d98837d780466/-/blob/rs/registry/canister/src/mutations/do_create_subnet.rs?L63)). This means that when the peer manager attempts to make connections to peers according to the "oldest registry version in use", the subnet doesn't actually exist yet, which then triggers the `empty_list_of_node_records` error. This is fine however, because it will find the correct peers in the subsequent registry version (which is guaranteed to exist, otherwise we would not have the registry CUP). To improve the error signal of this metric, we change it such that it is only incremented if _all_ registry versions do not contain transport infos, meaning there are no peers shared with the new subnet topology. --- rs/p2p/peer_manager/src/lib.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/rs/p2p/peer_manager/src/lib.rs b/rs/p2p/peer_manager/src/lib.rs index 89ff674f0b61..27723b386bf2 100644 --- a/rs/p2p/peer_manager/src/lib.rs +++ b/rs/p2p/peer_manager/src/lib.rs @@ -135,11 +135,6 @@ impl PeerManager { self.log, "Got transport infos but it's empty. Registry version {version}" ); - self.metrics - .topology_watcher_errors - .with_label_values(&["empty_list_of_node_records"]) - .inc(); - Vec::new() } Err(err) => { @@ -194,6 +189,13 @@ impl PeerManager { } } + if subnet_nodes.is_empty() { + self.metrics + .topology_watcher_errors + .with_label_values(&["empty_subnet_nodes"]) + .inc(); + } + SubnetTopology::new( subnet_nodes, earliest_registry_version, From 703257b56022be4058b8e9f9525460dee00e4c17 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Mon, 15 Jun 2026 10:08:32 +0200 Subject: [PATCH 30/75] chore: log `networkctl status enp2s0` when timing out on acquiring IPv4 for UVMs (#10464) All `//rs/tests/networking:canister_http...` tests are [very flaky](https://dash.dm1-idx1.dfinity.network/invocation/34f3e50b-0713-40e2-a983-b71373683092) due to their `httpbin` UVMs failing to acquire an IPv4 address in time in the `dm1` DC. To get a bit more insight into why this is the case we log `networkctl status enp2s0` on timeout. --- rs/tests/driver/src/driver/universal_vm.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rs/tests/driver/src/driver/universal_vm.rs b/rs/tests/driver/src/driver/universal_vm.rs index 351275f73c72..adf6f2f1afd7 100644 --- a/rs/tests/driver/src/driver/universal_vm.rs +++ b/rs/tests/driver/src/driver/universal_vm.rs @@ -351,6 +351,8 @@ until ipv4=$(ip -j address show dev enp2s0 \ do if [ "$count" -ge 300 ]; then echo "Timed out waiting for IPv4 address!" >&2 + echo "networkctl status enp2s0:" >&2 + networkctl status enp2s0 >&2 exit 1 fi sleep 1 From 0e9b0964924a87396d93afada170a5b538c324f2 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:13:50 +0200 Subject: [PATCH 31/75] fix: persistent_next_idx in log memory store for zero log memory limit (#10461) This PR fixes the `persistent_next_idx` in the new log memory store in case the log memory store is deallocated (e.g., if the log memory limit is set to zero or the canister runs out of cycles): before this PR, `persistent_next_idx` was not advanced in this case if the canister produced logs which leaves such canister logs undetectable; now `persistent_next_idx` is advanced and thus as soon as the log memory store is allocated, subsequent canister logs will be indexed with that advanced `persistent_next_idx` and the gap in log indices reveals that some canister logs were not observed (because the log memory store was deallocated). This PR is safe to roll out because the new log memory store is not migrated on any subnet by the time this PR is being rolled out. --- rs/execution_environment/tests/canister_logging.rs | 12 +++++------- .../system_state/log_memory_store/mod.rs | 5 ++++- .../log_memory_store/tests/canister_lifecycle.rs | 6 ++++-- .../log_memory_store/tests/log_memory_store.rs | 7 +++---- rs/state_manager/src/checkpoint.rs | 11 +++++++++++ 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/rs/execution_environment/tests/canister_logging.rs b/rs/execution_environment/tests/canister_logging.rs index 87d8fde13807..d61b141cba67 100644 --- a/rs/execution_environment/tests/canister_logging.rs +++ b/rs/execution_environment/tests/canister_logging.rs @@ -3060,23 +3060,21 @@ fn test_canister_log_with_zero_log_memory_limit() { .build(), ); - // Produce a second log with no ring buffer. canister_log advances to - // next_idx == 2, but persistent_next_idx stays at 1. + // Produce a second log with no ring buffer. Both canister_log and + // persistent_next_idx advance to next_idx == 2. let _ = env.execute_ingress( canister_id, "update", wasm().debug_print(b"log").reply().build(), ); - // canister_log.next_idx() == 2 and lms.next_idx() == 1 must hold before - // and after a checkpoint/reload cycle. Before the CanisterStateBits fix, - // the reload would wrongly restore lms.persistent_next_idx from - // next_canister_log_record_idx (== 2) instead of the (by now) stored 1. + // Both canister_log.next_idx() and lms.next_idx() must be 2 before + // and after a checkpoint/reload cycle. let check_next_idx = |env: &StateMachine| { let state = env.get_latest_state(); let ss = &state.canister_state(&canister_id).unwrap().system_state; assert_eq!(ss.canister_log.next_idx(), 2); - assert_eq!(ss.log_memory_store.next_idx(), 1); + assert_eq!(ss.log_memory_store.next_idx(), 2); }; check_next_idx(&env); diff --git a/rs/replicated_state/src/canister_state/system_state/log_memory_store/mod.rs b/rs/replicated_state/src/canister_state/system_state/log_memory_store/mod.rs index 4b7abea7671f..b7d9ea76f6a0 100644 --- a/rs/replicated_state/src/canister_state/system_state/log_memory_store/mod.rs +++ b/rs/replicated_state/src/canister_state/system_state/log_memory_store/mod.rs @@ -419,7 +419,10 @@ impl LogMemoryStore { return; } let Some(mut ring_buffer) = self.load_ring_buffer() else { - return; // No ring buffer exists. + // No ring buffer exists (e.g., log_memory_limit is zero), but still + // carry the monotone index forward so consumers can track progress. + self.persistent_next_idx = self.persistent_next_idx.max(delta_log.next_idx()); + return; }; // Append the delta records and persist the ring buffer. ring_buffer.append_log(delta_log.records_mut().drain(..)); diff --git a/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/canister_lifecycle.rs b/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/canister_lifecycle.rs index 82d674719d7d..3bdba11895aa 100644 --- a/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/canister_lifecycle.rs +++ b/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/canister_lifecycle.rs @@ -247,11 +247,13 @@ fn test_canister_uninstall_and_install_drops_logs_until_resized() { canister.uninstall_code(); canister.install_code(); - // Test that without settings update, log memory remains cleared and drops msg + // Test that without settings update, log memory remains cleared and drops msg. + // next_idx still advances (tracking the global monotone sequence) even though + // the record is dropped due to no allocated ring buffer. canister.log("Message 2 ignored"); assert_eq!(canister.fetch_canister_logs().len(), 0); assert_eq!(canister.log_memory_usage().get(), 0); - assert_eq!(canister.next_idx(), 1); + assert_eq!(canister.next_idx(), 2); } #[test] diff --git a/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/log_memory_store.rs b/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/log_memory_store.rs index 0a576e90d96a..0b608cfee89d 100644 --- a/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/log_memory_store.rs +++ b/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/log_memory_store.rs @@ -119,21 +119,20 @@ fn test_retention_across_lifecycle() { } #[test] -fn test_appending_to_uninitialized_store_is_no_op() { +fn test_appending_to_uninitialized_store_updates_next_idx() { let mut s = LogMemoryStore::new(TEST_LOG_MEMORY_STORE_FEATURE); let mut delta = CanisterLog::default_delta(); delta.add_record(1, b"data".to_vec()); - // Append without setting limit + // Append without setting limit: no data stored, but next_idx advances. s.append_delta_log(&mut delta); - // Should still be empty assert!(s.is_empty()); assert_eq!(s.memory_usage(), 0); assert_eq!(s.byte_capacity(), 0); assert_eq!(s.bytes_used(), 0); assert_eq!(s.records(None).len(), 0); - assert_eq!(s.next_idx(), 0); + assert_eq!(s.next_idx(), 1); } #[test] diff --git a/rs/state_manager/src/checkpoint.rs b/rs/state_manager/src/checkpoint.rs index b25ed258bb31..da71a8a15ac4 100644 --- a/rs/state_manager/src/checkpoint.rs +++ b/rs/state_manager/src/checkpoint.rs @@ -870,6 +870,17 @@ pub fn load_canister_state( metrics, ); + if system_state.log_memory_store.is_migrated() { + let lms_next_idx = system_state.log_memory_store.next_idx(); + let log_next_idx = system_state.canister_log.next_idx(); + if lms_next_idx != log_next_idx { + metrics.observe_broken_soft_invariant(format!( + "canister {canister_id}: log_memory_store.next_idx ({lms_next_idx}) \ + != canister_log.next_idx ({log_next_idx})", + )); + } + } + let canister_state = CanisterState { system_state, execution_state, From 0a4fabaad8aeec0031e432052f7fb9bfad845c22 Mon Sep 17 00:00:00 2001 From: "pr-creation-bot-dfinity-ic[bot]" <200595415+pr-creation-bot-dfinity-ic[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 08:41:10 +0000 Subject: [PATCH 32/75] chore: Update Mainnet IC revisions canisters file (#10468) Update mainnet system canisters revisions file to include the latest WASM version released on the mainnet. This PR is created automatically using [`mainnet_revisions.py`](https://github.com/dfinity/ic/blob/master/ci/src/mainnet_revisions/mainnet_revisions.py) Co-authored-by: CI Automation --- mainnet-canister-revisions.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mainnet-canister-revisions.json b/mainnet-canister-revisions.json index 57d048254b28..8b042b6c99e1 100644 --- a/mainnet-canister-revisions.json +++ b/mainnet-canister-revisions.json @@ -84,12 +84,12 @@ "sha256": "b443df3315902404b142d60f3cfd2f580181683310f6e6321b52de297deffcda" }, "internet_identity_backend": { - "sha256": "34e7c805a34696cae9471c10dc30797c4daf8a14ebd53067a39ba3b66c9fe2b4", - "tag": "release-2026-06-05" + "sha256": "917a42d0f8621ab59921b3ad320bdc2fd30951721e1687e7e5d1be035936fce9", + "tag": "release-2026-06-12" }, "internet_identity_frontend": { - "sha256": "208f1415ddffe00af2c10cdd07cfc8f747f487dd8011839e85514e5ca50be8de", - "tag": "release-2026-06-05" + "sha256": "7f9e8557d25544dc00e72afc0f0c677e9823d66d07f8e65b671c102a44170c35", + "tag": "release-2026-06-12" }, "ledger": { "rev": "69b755062f5ef0a7d6efc9a127172b46121420c8", @@ -100,8 +100,8 @@ "sha256": "0d9221e28781e8b627c0e0696b16c0301424d4387514ed5fdae4fa74ad4b696b" }, "migration": { - "rev": "789e5a187985cedc654988f0f337467d6d5b3dcb", - "sha256": "a7c87d163a0cc69f6a5e4ed11f9a99618bde8658d21e8d84207963bb2400347c" + "rev": "8facd5635c5e05de9b423b64aeabc2e1ad58d66e", + "sha256": "7d8799644622662dc2eeb31d157c4f1d594d16fb91189396ce765121e750a5d7" }, "nns_dapp_test": { "sha256": "2eba384142418f6f4e4eff1326119dbc59be29b9e5e0adcdca58a8df727f00cd", From a043ed8cc53707f3119ec1812c7db899e6a50be8 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:51:35 +0200 Subject: [PATCH 33/75] fix: clean up registry after deleting subnet from PocketIC (#10428) This PR fixes subnet deletion in PocketIC by removing stale (subnet) registry records (orphaned after removing the subnet from the subnet list and routing table) as well as updating threshold signing registry records (defense-in-depth since threshold keys are only on "named" subnets which cannot be deleted). --- rs/pocket_ic_server/src/pocket_ic.rs | 20 ++- rs/state_machine_tests/src/lib.rs | 57 ++++++- rs/state_machine_tests/src/tests.rs | 240 +++++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 4 deletions(-) diff --git a/rs/pocket_ic_server/src/pocket_ic.rs b/rs/pocket_ic_server/src/pocket_ic.rs index eb838c4b0baf..c28e784b9ce3 100644 --- a/rs/pocket_ic_server/src/pocket_ic.rs +++ b/rs/pocket_ic_server/src/pocket_ic.rs @@ -110,7 +110,8 @@ use ic_sns_wasm::pb::v1::{AddWasmRequest, AddWasmResponse, SnsCanisterType, SnsW use ic_state_machine_tests::{ FakeVerifier, StateMachine, StateMachineBuilder, StateMachineConfig, StateMachineStateDir, SubmitIngressError, Subnets, WasmResult, add_global_registry_records, - add_initial_registry_records, update_global_registry_records, + add_initial_registry_records, remove_chain_key_registry_records, + remove_subnet_local_registry_records, update_global_registry_records, }; use ic_state_manager::StateManagerImpl; use ic_types::batch::BlockmakerMetrics; @@ -2801,6 +2802,12 @@ impl PocketIcSubnets { for subnets in self.chain_keys.values_mut() { subnets.retain(|&sid| sid != subnet_id); } + let empty_chain_key_ids: Vec = self + .chain_keys + .iter() + .filter(|(_, subnets)| subnets.is_empty()) + .map(|(key_id, _)| key_id.clone()) + .collect(); self.chain_keys.retain(|_, subnets| !subnets.is_empty()); // Delete the subnet state directory from disk. @@ -2822,6 +2829,11 @@ impl PocketIcSubnets { if self.nns_subnet.is_some() { let next_version = RegistryVersion::new(self.registry_data_provider.latest_version().get() + 1); + remove_chain_key_registry_records( + &empty_chain_key_ids, + self.registry_data_provider.clone(), + next_version, + ); let subnet_list = self .subnets .get_all() @@ -2835,6 +2847,12 @@ impl PocketIcSubnets { self.chain_keys.clone(), self.registry_data_provider.clone(), ); + remove_subnet_local_registry_records( + subnet_id, + &subnet.state_machine.nodes, + self.registry_data_provider.clone(), + next_version, + ); self.persist_registry_changes(); } diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 6c075cd9bb9a..2e4bd98268e5 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -50,7 +50,7 @@ use ic_interfaces::{ use ic_interfaces_certified_stream_store::{ CertifiedStreamStore, DecodeStreamError, EncodeStreamError, }; -use ic_interfaces_registry::RegistryClient; +use ic_interfaces_registry::{RegistryClient, RegistryRecord}; use ic_interfaces_state_manager::{ CertificationScope, CertifiedStateSnapshot, Labeled, StateHashError, StateHashMetadata, StateManager, StateReader, @@ -101,8 +101,9 @@ use ic_registry_client_helpers::{ use ic_registry_keys::{ NODE_REWARDS_TABLE_KEY, ROOT_SUBNET_ID_KEY, make_canister_migrations_record_key, make_canister_ranges_key, make_catch_up_package_contents_key, - make_chain_key_enabled_subnet_list_key, make_crypto_node_key, make_crypto_tls_cert_key, - make_node_record_key, make_provisional_whitelist_record_key, make_replica_version_key, + make_chain_key_enabled_subnet_list_key, make_crypto_node_key, + make_crypto_threshold_signing_pubkey_key, make_crypto_tls_cert_key, make_node_record_key, + make_provisional_whitelist_record_key, make_replica_version_key, make_subnet_record_key, }; use ic_registry_proto_data_provider::{INITIAL_REGISTRY_VERSION, ProtoRegistryDataProvider}; use ic_registry_provisional_whitelist::ProvisionalWhitelist; @@ -522,6 +523,56 @@ fn add_subnet_local_registry_records( ); } +pub fn remove_subnet_local_registry_records( + subnet_id: SubnetId, + nodes: &[StateMachineNode], + registry_data_provider: Arc, + registry_version: RegistryVersion, +) { + let mut keys = vec![ + make_catch_up_package_contents_key(subnet_id), + make_crypto_threshold_signing_pubkey_key(subnet_id), + make_subnet_record_key(subnet_id), + ]; + for node in nodes { + keys.push(make_node_record_key(node.node_id)); + keys.push(make_crypto_tls_cert_key(node.node_id)); + for key_purpose in [ + KeyPurpose::NodeSigning, + KeyPurpose::CommitteeSigning, + KeyPurpose::DkgDealingEncryption, + KeyPurpose::IDkgMEGaEncryption, + ] { + keys.push(make_crypto_node_key(node.node_id, key_purpose)); + } + } + let records = keys + .into_iter() + .map(|key| RegistryRecord { + key, + version: registry_version, + value: None, + }) + .collect(); + registry_data_provider.add_registry_records(records); +} + +pub fn remove_chain_key_registry_records( + chain_key_ids: &[MasterPublicKeyId], + registry_data_provider: Arc, + registry_version: RegistryVersion, +) { + let records = chain_key_ids + .iter() + .map(|key_id| RegistryRecord { + key: make_chain_key_enabled_subnet_list_key(key_id), + version: registry_version, + value: None, + }) + .collect(); + registry_data_provider.add_registry_records(records); +} + fn add_cup_contents_and_key_record( subnet_id: SubnetId, ni_dkg_transcript: NiDkgTranscript, diff --git a/rs/state_machine_tests/src/tests.rs b/rs/state_machine_tests/src/tests.rs index c968fedeb220..3f8cfca0cd7e 100644 --- a/rs/state_machine_tests/src/tests.rs +++ b/rs/state_machine_tests/src/tests.rs @@ -1,6 +1,246 @@ use ic_secp256k1::{DerivationIndex, DerivationPath, PrivateKey, PublicKey}; use proptest::{collection::vec as pvec, prelude::*, prop_assert}; +#[test] +fn test_remove_subnet_local_registry_records() { + use ic_crypto_test_utils_ni_dkg::dummy_initial_dkg_transcript_with_master_key; + use ic_crypto_utils_threshold_sig_der::threshold_sig_public_key_to_der; + use ic_interfaces_registry::{RegistryDataProvider, ZERO_REGISTRY_VERSION}; + use ic_management_canister_types_private::{ + EcdsaCurve, EcdsaKeyId, MasterPublicKeyId, SchnorrAlgorithm, SchnorrKeyId, VetKdCurve, + VetKdKeyId, + }; + use ic_registry_keys::{ + NODE_REWARDS_TABLE_KEY, ROOT_SUBNET_ID_KEY, make_canister_ranges_key, + make_chain_key_enabled_subnet_list_key, make_provisional_whitelist_record_key, + make_replica_version_key, make_subnet_list_record_key, + }; + use ic_registry_proto_data_provider::{INITIAL_REGISTRY_VERSION, ProtoRegistryDataProvider}; + use ic_registry_resource_limits::ResourceLimits; + use ic_registry_routing_table::RoutingTable; + use ic_registry_subnet_features::SubnetFeatures; + use ic_registry_subnet_type::SubnetType; + use ic_types::{CanisterId, PrincipalId, ReplicaVersion, SubnetId}; + use ic_types_cycles::CanisterCyclesCostSchedule; + use rand::SeedableRng; + use rand::rngs::StdRng; + use std::collections::{BTreeMap, HashMap, HashSet}; + use std::sync::Arc; + + let seed = [42_u8; 32]; + let mut node_rng = StdRng::from_seed(seed); + let nodes: Vec = (0..4) + .map(|_| super::StateMachineNode::new(&mut node_rng)) + .collect(); + let (ni_dkg_transcript, _) = + dummy_initial_dkg_transcript_with_master_key(&mut StdRng::from_seed(seed)); + let public_key = (&ni_dkg_transcript).try_into().unwrap(); + let public_key_der = threshold_sig_public_key_to_der(public_key).unwrap(); + let subnet_id = PrincipalId::new_self_authenticating(&public_key_der).into(); + + let chain_key_ids: Vec = vec![ + MasterPublicKeyId::Ecdsa(EcdsaKeyId { + curve: EcdsaCurve::Secp256k1, + name: "test_ecdsa_key".to_string(), + }), + MasterPublicKeyId::Schnorr(SchnorrKeyId { + algorithm: SchnorrAlgorithm::Ed25519, + name: "test_schnorr_key".to_string(), + }), + MasterPublicKeyId::VetKd(VetKdKeyId { + curve: VetKdCurve::Bls12_381_G2, + name: "test_vetkd_key".to_string(), + }), + ]; + let chain_keys_enabled_status: BTreeMap = chain_key_ids + .iter() + .map(|key_id| (key_id.clone(), true)) + .collect(); + let chain_keys: BTreeMap> = chain_key_ids + .iter() + .map(|key_id| (key_id.clone(), vec![subnet_id])) + .collect(); + + let registry_data_provider = Arc::new(ProtoRegistryDataProvider::new()); + super::add_initial_registry_records(registry_data_provider.clone()); + super::add_global_registry_records( + subnet_id, + RoutingTable::new(), + vec![subnet_id], + chain_keys, + registry_data_provider.clone(), + ); + super::add_subnet_local_registry_records( + subnet_id, + SubnetType::Application, + SubnetFeatures::default(), + &nodes, + public_key, + &chain_keys_enabled_status, + ni_dkg_transcript, + registry_data_provider.clone(), + INITIAL_REGISTRY_VERSION, + CanisterCyclesCostSchedule::Normal, + vec![], + ResourceLimits::default(), + ); + + let global_keys: HashSet = HashSet::from([ + ROOT_SUBNET_ID_KEY.to_string(), + NODE_REWARDS_TABLE_KEY.to_string(), + make_canister_ranges_key(CanisterId::from_u64(0)), + make_subnet_list_record_key(), + make_provisional_whitelist_record_key(), + make_replica_version_key(ReplicaVersion::default()), + make_chain_key_enabled_subnet_list_key(&MasterPublicKeyId::Ecdsa(EcdsaKeyId { + curve: EcdsaCurve::Secp256k1, + name: "test_ecdsa_key".to_string(), + })), + make_chain_key_enabled_subnet_list_key(&MasterPublicKeyId::Schnorr(SchnorrKeyId { + algorithm: SchnorrAlgorithm::Ed25519, + name: "test_schnorr_key".to_string(), + })), + make_chain_key_enabled_subnet_list_key(&MasterPublicKeyId::VetKd(VetKdKeyId { + curve: VetKdCurve::Bls12_381_G2, + name: "test_vetkd_key".to_string(), + })), + ]); + + let remove_version = INITIAL_REGISTRY_VERSION.increment(); + super::remove_subnet_local_registry_records( + subnet_id, + &nodes, + registry_data_provider.clone(), + remove_version, + ); + + // Build a map: key -> latest value at or before remove_version. + let mut latest: HashMap>> = HashMap::new(); + let mut records: Vec<_> = registry_data_provider + .get_updates_since(ZERO_REGISTRY_VERSION) + .unwrap() + .into_iter() + .filter(|r| r.version <= remove_version) + .collect(); + records.sort_by_key(|r| r.version); + for r in records { + latest.insert(r.key, r.value); + } + + // Global/initial keys must still have values; all other keys must be tombstoned. + for (key, value) in &latest { + if global_keys.contains(key) { + assert!( + value.is_some(), + "global/initial key '{}' should still exist but was removed", + key + ); + } else { + assert!( + value.is_none(), + "subnet-local key '{}' should be removed but still has a value", + key + ); + } + } + // Every global/initial key must still be present. + for key in &global_keys { + assert!( + latest.contains_key(key), + "global/initial key '{}' not found in registry", + key + ); + } +} + +#[test] +fn test_remove_chain_key_registry_records() { + use ic_interfaces_registry::{RegistryDataProvider, ZERO_REGISTRY_VERSION}; + use ic_management_canister_types_private::{ + EcdsaCurve, EcdsaKeyId, MasterPublicKeyId, SchnorrAlgorithm, SchnorrKeyId, VetKdCurve, + VetKdKeyId, + }; + use ic_registry_keys::make_chain_key_enabled_subnet_list_key; + use ic_registry_proto_data_provider::{INITIAL_REGISTRY_VERSION, ProtoRegistryDataProvider}; + use ic_registry_routing_table::RoutingTable; + use ic_types::{PrincipalId, SubnetId}; + use std::collections::{BTreeMap, HashMap}; + use std::sync::Arc; + + let subnet_id: SubnetId = PrincipalId::new_subnet_test_id(1).into(); + + let all_key_ids: Vec = vec![ + MasterPublicKeyId::Ecdsa(EcdsaKeyId { + curve: EcdsaCurve::Secp256k1, + name: "test_ecdsa_key".to_string(), + }), + MasterPublicKeyId::Schnorr(SchnorrKeyId { + algorithm: SchnorrAlgorithm::Ed25519, + name: "test_schnorr_key".to_string(), + }), + MasterPublicKeyId::VetKd(VetKdKeyId { + curve: VetKdCurve::Bls12_381_G2, + name: "test_vetkd_key".to_string(), + }), + ]; + + let registry_data_provider = Arc::new(ProtoRegistryDataProvider::new()); + + // Add chain key records at the initial version. + let chain_keys: BTreeMap> = all_key_ids + .iter() + .map(|key_id| (key_id.clone(), vec![subnet_id])) + .collect(); + super::update_global_registry_records( + INITIAL_REGISTRY_VERSION, + RoutingTable::new(), + vec![], + chain_keys, + registry_data_provider.clone(), + ); + + // Remove only the first two keys (ECDSA and Schnorr) at version 2. + let keys_to_remove = &all_key_ids[..2]; + let remove_version = INITIAL_REGISTRY_VERSION.increment(); + super::remove_chain_key_registry_records( + keys_to_remove, + registry_data_provider.clone(), + remove_version, + ); + + // Build map: key -> latest value at or before remove_version. + let mut latest: HashMap>> = HashMap::new(); + let mut records: Vec<_> = registry_data_provider + .get_updates_since(ZERO_REGISTRY_VERSION) + .unwrap() + .into_iter() + .filter(|r| r.version <= remove_version) + .collect(); + records.sort_by_key(|r| r.version); + for r in records { + latest.insert(r.key, r.value); + } + + // The removed keys must be tombstoned (value == None). + for key_id in keys_to_remove { + let reg_key = make_chain_key_enabled_subnet_list_key(key_id); + assert_eq!( + latest.get(®_key), + Some(&None), + "removed key '{}' should be tombstoned", + reg_key + ); + } + + // The remaining key (VetKD) must still have a value. + let kept_key = make_chain_key_enabled_subnet_list_key(&all_key_ids[2]); + assert!( + latest.get(&kept_key).and_then(|v| v.as_ref()).is_some(), + "non-removed key '{}' should still exist", + kept_key + ); +} + #[test_strategy::proptest] fn test_derivation_prop( #[strategy(pvec(pvec(any::(), 1..10), 1..10))] derivation_path_bytes: Vec>, From a91ae4e034c911ab76bfcf079407773bf6aec420 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:52:01 +0200 Subject: [PATCH 34/75] fix: gaps in canister logs (#10463) This PR makes sure that canister logs reported via `fetch_canister_logs` do not contain gaps in terms of log record indices. This is achieved by clearing the aggregate log if the delta log (produced by the latest message) starts with a gap. The gaps currently arise from the fact that the delta log is collected and wrapped around in the canister sandbox separately from the aggregate log, but this is an implementation detail. If the canister sandbox appended logs directly to the delta log and wrapped it around, no gaps would arise. This PR achieves that without appending logs in the canister sandbox directly to the delta log by observing that if the delta log wraps around, then the aggregated log would have wrapped around exactly like that. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../tests/canister_logging.rs | 50 +++++++++++++++++++ .../system_state/log_memory_store/mod.rs | 8 +++ .../tests/log_memory_store.rs | 32 ++++++++++++ rs/types/types/src/canister_log.rs | 22 ++++---- 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/rs/execution_environment/tests/canister_logging.rs b/rs/execution_environment/tests/canister_logging.rs index d61b141cba67..2e21a8fa6c3f 100644 --- a/rs/execution_environment/tests/canister_logging.rs +++ b/rs/execution_environment/tests/canister_logging.rs @@ -1011,6 +1011,56 @@ fn test_canister_log_in_state_stays_within_limit() { assert_le!(log_size, TEST_DEFAULT_LOG_MEMORY_LIMIT); } +#[test] +fn test_canister_log_overflow_evicts_oldest_records() { + // First update: 2 debug prints of 8 bytes (indices 0, 1). + // Second update: 16 debug prints of 256 bytes (indices 2..17). + // + // With DEFAULT_AGGREGATE_LOG_MEMORY_LIMIT = 4096 bytes: + // data_size(8) = 40 + 8 = 48 bytes + // data_size(256) = 40 + 256 = 296 bytes + // + // The delta for the second update has capacity 4096 bytes and holds + // floor(4096 / 296) = 13 records (indices 5..17, bytes_used = 3848). + // + // Appending the delta to the aggregate (96 bytes for records 0 and 1): + // delta.first.idx (5) > aggregate.next_idx (2) => gap detected => aggregate cleared. + // + // Result: records 5..17 (256-byte content) = 13 records. + let user_controller = PrincipalId::new_user_test_id(42); + let (env, canister_id) = setup_with_controller( + user_controller, + wat_canister() + .update( + "update1", + wat_fn().debug_print(&[1_u8; 8]).debug_print(&[1_u8; 8]), + ) + .update( + "update2", + wat_fn().repeat(16, wat_fn().debug_print(&[2_u8; 256])), + ) + .build_wasm(), + ); + + env.advance_time(Duration::from_secs(1)); + let _ = env.execute_ingress(canister_id, "update1", vec![]); + env.advance_time(Duration::from_secs(1)); + let _ = env.execute_ingress(canister_id, "update2", vec![]); + + let records = fetch_log_records(&env, user_controller, canister_id); + // The delta evicted records idx 2, 3, 4 (16 prints > floor(4096/296)=13 capacity), + // so the delta starts at idx 5 > aggregate next_idx 2. Both first-update records + // are dropped to maintain index continuity. Result: 13 records with 256-byte content. + assert_eq!(records.len(), 13); + for record in &records { + assert_eq!(record.content, vec![2_u8; 256]); + } + // First record has idx 5 = 2 + (16 - 13) (second update starts at idx 2). + assert_eq!(records[0].idx, 5); + // Last record has idx 17 = 2 + 16 - 1. + assert_eq!(records.last().unwrap().idx, 17); +} + #[test] fn test_logging_trap_in_heartbeat() { let user_controller = PrincipalId::new_user_test_id(42); diff --git a/rs/replicated_state/src/canister_state/system_state/log_memory_store/mod.rs b/rs/replicated_state/src/canister_state/system_state/log_memory_store/mod.rs index b7d9ea76f6a0..a83d6297b578 100644 --- a/rs/replicated_state/src/canister_state/system_state/log_memory_store/mod.rs +++ b/rs/replicated_state/src/canister_state/system_state/log_memory_store/mod.rs @@ -424,6 +424,14 @@ impl LogMemoryStore { self.persistent_next_idx = self.persistent_next_idx.max(delta_log.next_idx()); return; }; + // If the delta overflowed and evicted records, there is a gap between the + // aggregate's next expected index and the delta's first record. Clear the + // ring buffer to maintain index continuity. + if let Some(first) = delta_log.records().front() + && first.idx > self.next_idx() + { + ring_buffer.clear(); + } // Append the delta records and persist the ring buffer. ring_buffer.append_log(delta_log.records_mut().drain(..)); self.save_ring_buffer(ring_buffer); diff --git a/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/log_memory_store.rs b/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/log_memory_store.rs index 0b608cfee89d..59450deabe51 100644 --- a/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/log_memory_store.rs +++ b/rs/replicated_state/src/canister_state/system_state/log_memory_store/tests/log_memory_store.rs @@ -846,3 +846,35 @@ fn memory_usage_for_limit_at_minimum() { fn memory_usage_for_limit_above_minimum() { assert_memory_usage_for_limit(TEST_LOG_MEMORY_STORE_FEATURE, TEST_LOG_MEMORY_LIMIT); } + +#[test] +fn test_gap_in_delta_clears_store() { + // Simulate a delta that overflowed its capacity and evicted older records, + // creating a gap between the store's next_idx and the delta's first record. + // + // Setup: store has records idx 0 and 1 (next_idx == 2). + // Delta: starts at idx 5 (gap: 2, 3, 4 were evicted from the delta). + // Expected: store is cleared before appending, so only idx 5 remains. + let mut s = LogMemoryStore::new(TEST_LOG_MEMORY_STORE_FEATURE); + s.resize_for_testing(TEST_LOG_MEMORY_LIMIT); + + // Populate the store with records 0 and 1. + let mut initial = CanisterLog::new_delta_with_next_index(0, TEST_LOG_MEMORY_LIMIT); + initial.add_record(100, b"store #0".to_vec()); + initial.add_record(101, b"store #1".to_vec()); + s.append_delta_log(&mut initial); + assert_eq!(s.next_idx(), 2); + assert_eq!(s.records(None).len(), 2); + + // Build a delta that starts at idx 5 (gap: 2, 3, 4 evicted). + let mut delta = CanisterLog::new_delta_with_next_index(5, TEST_LOG_MEMORY_LIMIT); + delta.add_record(202, b"delta #2".to_vec()); + s.append_delta_log(&mut delta); + + // The gap (5 > 2) triggers a clear; only the delta record survives. + let records = s.records(None); + assert_eq!(records.len(), 1); + assert_eq!(records[0].idx, 5); + assert_eq!(records[0].content, b"delta #2"); + assert_eq!(s.next_idx(), 6); +} diff --git a/rs/types/types/src/canister_log.rs b/rs/types/types/src/canister_log.rs index 2c68328e09e7..aefd27a867f3 100644 --- a/rs/types/types/src/canister_log.rs +++ b/rs/types/types/src/canister_log.rs @@ -245,7 +245,16 @@ impl CanisterLog { return; // Don't append if delta is empty. } - // Assume records sorted cronologically (with increasing idx) and + // If the delta overflowed and evicted records, there is a gap between the + // aggregate's next expected index and the delta's first record. Drop all + // aggregate records to maintain index continuity. + if let Some(first) = delta_log.records.get().front() + && first.idx > self.next_idx + { + self.records.clear(); + } + + // Assume records sorted chronologically (with increasing idx) and // update the system state's next index with the last record's index. if let Some(last) = delta_log.records.get().back() { self.next_idx = last.idx + 1; @@ -417,16 +426,11 @@ mod tests { // Act. main.append_delta_log(&mut delta); - // Assert main log had data loss. + // Assert main log was cleared due to the gap (delta evicted records 3 and 4, + // so delta starts at idx 5 > next_idx 3; aggregate records are dropped). assert_eq!( main.records(), - &VecDeque::from(canister_log_records(&[ - (0, 100, b"main #0"), - (1, 101, b"main #1"), - (2, 102, b"main #2"), - // Expected data loss. - (5, 202, b"delta #2"), - ])) + &VecDeque::from(canister_log_records(&[(5, 202, b"delta #2"),])) ); } From b8449265c1c1ddd9403a9458b124d528bebf433f Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Mon, 15 Jun 2026 10:55:45 +0200 Subject: [PATCH 35/75] fix: deflake //rs/tests/consensus/orchestrator:ssh_access_to_nodes_test (#10460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Deflakes `//rs/tests/consensus/orchestrator:ssh_access_to_nodes_test`, whose subtest `node_does_not_remove_keys_on_restart` was flaky. ## Root cause All recent flaky runs failed identically: a panic at `rs/tests/consensus/utils/src/ssh_access.rs:101` (the `.unwrap()` in `assert_authentication_works`) with: ``` [Session(-43)] Failed getting banner ``` `Session(-43)` is `LIBSSH2_ERROR_SOCKET_RECV` and *"Failed getting banner"* means the SSH **transport** handshake (`session.handshake()`) failed **before any authentication took place**. This is a transient transport-level hiccup that occurs when the node is briefly unresponsive — in particular right after `sudo systemctl restart ic-replica`, which is exactly what this subtest does before repeatedly asserting (in two polling loops) that SSH access still works. The SSH keys were present the entire time (that is precisely what the subtest verifies), so this was a **false failure**: a transport hiccup, not a missing key. Because `assert_authentication_works` did a single connect + handshake + auth with **no retry**, one transient transport failure was enough to panic the whole test. This matches all 4 flaky runs observed in the last week, which always: - failed in `node_does_not_remove_keys_on_restart`, - panicked at `ssh_access.rs:101` with `Failed getting banner`, - did so during the post-restart "still accept connections" polling loops. ## Fix Split `SshSession::login` into two steps: - `connect` — TCP connect + SSH transport handshake (the transport layer). - `authenticate` — user authentication (reflects whether the key actually grants access). `assert_authentication_works` now retries only the transport `connect` step for a bounded time (the existing `SSH_ACCESS_TIMEOUT` of 30s with `SSH_ACCESS_BACKOFF` of 5s), then authenticates **exactly once**. Transient transport failures are absorbed, while a genuine loss of access (e.g. a key being incorrectly removed) still fails immediately, because authentication is not retried. The public `login` signature is unchanged, so the other callers are unaffected. ## Verification `bazel test --test_output=errors --runs_per_test=3 --jobs=3 --cache_test_results=no //rs/tests/consensus/orchestrator:ssh_access_to_nodes_test` 2 of 3 runs passed fully, including `node_does_not_remove_keys_on_restart`, and the `Failed getting banner` panic did not recur. The 1 remaining failure was an unrelated `Retried too many times: sending a request to Farm` infrastructure timeout during setup, which is explicitly ignored per the flaky-test guide. --- This PR was created following the steps in `.claude/skills/fix-flaky-tests/SKILL.md`. --- rs/tests/consensus/utils/src/ssh_access.rs | 29 +++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/rs/tests/consensus/utils/src/ssh_access.rs b/rs/tests/consensus/utils/src/ssh_access.rs index 54324e625098..fa7af13f40e6 100644 --- a/rs/tests/consensus/utils/src/ssh_access.rs +++ b/rs/tests/consensus/utils/src/ssh_access.rs @@ -81,10 +81,33 @@ impl Default for SshSession { impl SshSession { pub fn login(&mut self, ip: &IpAddr, username: &str, mean: &AuthMean) -> Result<(), String> { + // The SSH transport handshake (TCP connect + protocol banner and key + // exchange) is retried for a bounded time because it can fail transiently + // (e.g. with "Failed getting banner") when the node is briefly unresponsive, + // for instance right after the replica/orchestrator is restarted. Such + // transport-level failures are unrelated to whether the key grants access, + // which is what the callers actually test. The authentication step below is + // not retried, so an actual loss of access (e.g. a key being incorrectly + // removed) is still reported immediately. let ip_str = format!("[{ip}]:22"); - let tcp = TcpStream::connect(ip_str).map_err(|err| err.to_string())?; - self.session.set_tcp_stream(tcp); - self.session.handshake().map_err(|err| err.to_string())?; + let start = std::time::Instant::now(); + loop { + let handshake_result = TcpStream::connect(&ip_str) + .map_err(|err| err.to_string()) + .and_then(|tcp| { + self.session.set_tcp_stream(tcp); + self.session.handshake().map_err(|err| err.to_string()) + }); + match handshake_result { + Ok(()) => break, + Err(_) if start.elapsed() < SSH_ACCESS_TIMEOUT => { + std::thread::sleep(SSH_ACCESS_BACKOFF); + // A failed handshake can leave the session unusable, so replace it. + self.session = Session::new().unwrap(); + } + Err(err) => return Err(err), + } + } match mean { AuthMean::PrivateKey(pk) => self From ceadcbeeacc217c3b07ccab25582118360201002 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:03:07 +0200 Subject: [PATCH 36/75] fix: filter canister log records during migration to new log memory store (#10462) This PR filters canister log records during their migration to the new log memory store to ensure that the indices of the migrated logs are consecutive integers ending at `next_idx - 1`. --- rs/execution_environment/src/scheduler.rs | 179 +++++++++++++++++- .../tests/canister_logging.rs | 98 +++++++++- 2 files changed, 271 insertions(+), 6 deletions(-) diff --git a/rs/execution_environment/src/scheduler.rs b/rs/execution_environment/src/scheduler.rs index 476f2541be5f..3eda6c33c653 100644 --- a/rs/execution_environment/src/scheduler.rs +++ b/rs/execution_environment/src/scheduler.rs @@ -26,7 +26,9 @@ use ic_interfaces::execution_environment::{ IngressHistoryWriter, Scheduler, SubnetAvailableMemory, }; use ic_logger::{ReplicaLogger, debug, error, fatal, info, new_logger, warn}; -use ic_management_canister_types_private::{CanisterStatusType, Method as Ic00Method}; +use ic_management_canister_types_private::{ + CanisterLogRecord, CanisterStatusType, Method as Ic00Method, +}; use ic_metrics::MetricsRegistry; use ic_registry_resource_limits::ResourceLimits; use ic_registry_subnet_type::SubnetType; @@ -41,14 +43,14 @@ use ic_types::batch::ChainKeyData; use ic_types::ingress::{IngressState, IngressStatus}; use ic_types::messages::{Ingress, MessageId, NO_DEADLINE, Response, SubnetMessage}; use ic_types::{ - CanisterId, ComputeAllocation, DEFAULT_AGGREGATE_LOG_MEMORY_LIMIT, ExecutionRound, + CanisterId, CanisterLog, ComputeAllocation, DEFAULT_AGGREGATE_LOG_MEMORY_LIMIT, ExecutionRound, MemoryAllocation, NumBytes, NumInstructions, NumMessages, NumSlices, Randomness, ReplicaVersion, Time, }; use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles}; use more_asserts::{debug_assert_ge, debug_assert_le, debug_assert_lt}; use std::cell::RefCell; -use std::collections::BTreeSet; +use std::collections::{BTreeSet, VecDeque}; use std::str::FromStr; use std::sync::Arc; use strum::IntoEnumIterator; @@ -1139,7 +1141,7 @@ impl Scheduler for SchedulerImpl { .round_log_memory_store_migration_duration .start_timer(); let log_memory_store_feature = self.log_memory_store_feature; - state.canisters_for_each_mut(|_id, canister| { + state.canisters_for_each_mut(|canister_id, canister| { if log_memory_store_feature == FlagStatus::Enabled && !canister.system_state.log_memory_store.is_migrated() { @@ -1148,9 +1150,18 @@ impl Scheduler for SchedulerImpl { DEFAULT_AGGREGATE_LOG_MEMORY_LIMIT, Arc::clone(&self.fd_factory), ); + let canister_log = &system_state.canister_log; + let next_idx = canister_log.next_idx(); + let records = filter_canister_log_records( + canister_log.records(), + next_idx, + *canister_id, + &self.log, + ); + let mut filtered_log = CanisterLog::new_aggregate(next_idx, records); system_state .log_memory_store - .append_delta_log(&mut system_state.canister_log.clone()); + .append_delta_log(&mut filtered_log); system_state.log_memory_store.set_migrated(); } else if log_memory_store_feature == FlagStatus::Disabled && canister.system_state.log_memory_store.is_migrated() @@ -2133,6 +2144,63 @@ fn subnet_heap_delta_capacity( .unwrap_or(config.subnet_heap_delta_capacity) } +/// Filters `records` to the contiguous suffix ending at `next_idx - 1`, +/// logging and discarding any records with invalid or gap-preceding indices. +fn filter_canister_log_records( + records: &VecDeque, + next_idx: u64, + canister_id: CanisterId, + log: &ReplicaLogger, +) -> Vec { + let warn_drop = |record: &CanisterLogRecord, reason: &str| { + warn!( + log, + "Canister {}: dropping log record with idx {} ({} next_idx {}), \ + timestamp {}, content \"{}\"", + canister_id, + record.idx, + reason, + next_idx, + record.timestamp_nanos, + String::from_utf8_lossy(&record.content), + ); + }; + for record in records.iter().filter(|r| r.idx >= next_idx) { + warn_drop(record, ">="); + } + let mut filtered: Vec<_> = records.iter().cloned().collect(); + filtered.retain(|r| r.idx < next_idx); + // Keep only the contiguous suffix ending at next_idx - 1, + // discarding any earlier records that precede a gap. + if next_idx > 0 { + if filtered.last().map(|r| r.idx) != Some(next_idx - 1) { + for record in &filtered { + warn_drop(record, "gap before"); + } + filtered.clear(); + } else { + let mut expected = next_idx - 1; + let mut contiguous_start = filtered.len() - 1; + for i in (0..filtered.len() - 1).rev() { + if expected == 0 { + break; + } + if filtered[i].idx == expected - 1 { + expected -= 1; + contiguous_start = i; + } else { + break; + } + } + for record in filtered.iter().take(contiguous_start) { + warn_drop(record, "gap before"); + } + filtered.drain(..contiguous_start); + } + } + filtered +} + /// Aborts the paused execution, if any, of the given canister. /// /// If a paused execution was aborted, resets the canister's executed rounds to @@ -2167,3 +2235,104 @@ pub fn abort_all_paused_executions( abort_canister(canister, subnet_schedule, exec_env, cost_schedule, log); } } + +#[cfg(test)] +mod canister_log_filter_tests { + use super::filter_canister_log_records; + use ic_logger::no_op_logger; + use ic_management_canister_types_private::CanisterLogRecord; + use ic_types_test_utils::ids::canister_test_id; + use std::collections::VecDeque; + + fn make_records(idxs: &[u64]) -> VecDeque { + idxs.iter() + .map(|&idx| CanisterLogRecord { + idx, + timestamp_nanos: idx * 1_000, + content: format!("record {idx}").into_bytes(), + }) + .collect() + } + + fn filtered_idxs(records: &VecDeque, next_idx: u64) -> Vec { + filter_canister_log_records(records, next_idx, canister_test_id(0), &no_op_logger()) + .into_iter() + .map(|r| r.idx) + .collect() + } + + #[test] + fn test_filter_single_record_from_zero() { + let records = make_records(&[0]); + assert_eq!(filtered_idxs(&records, 1), vec![0]); + } + + #[test] + fn test_filter_single_record_nonzero() { + let records = make_records(&[42]); + assert_eq!(filtered_idxs(&records, 43), vec![42]); + } + + #[test] + fn test_filter_duplicate_zero() { + // Two records at idx=0: walk breaks immediately (expected==0), first is dropped. + let records = make_records(&[0, 0]); + assert_eq!(filtered_idxs(&records, 1), vec![0]); + } + + #[test] + fn test_filter_duplicate_nonzero() { + // Two records at idx=42: 42 != expected-1=41, breaks, first is dropped. + let records = make_records(&[42, 42]); + assert_eq!(filtered_idxs(&records, 43), vec![42]); + } + + #[test] + fn test_filter_leading_duplicate_then_consecutive() { + // Walk reaches the second 0 (0==expected-1=0), then expected==0 breaks; first 0 dropped. + let records = make_records(&[0, 0, 1, 2]); + assert_eq!(filtered_idxs(&records, 3), vec![0, 1, 2]); + } + + #[test] + fn test_filter_duplicate_in_middle() { + // Walk: 3←2←1 (second 1), then first 1 != expected-1=0; first two records dropped. + let records = make_records(&[0, 1, 1, 2, 3]); + assert_eq!(filtered_idxs(&records, 4), vec![1, 2, 3]); + } + + #[test] + fn test_filter_consecutive_from_zero() { + let records = make_records(&[0, 1, 2]); + assert_eq!(filtered_idxs(&records, 3), vec![0, 1, 2]); + } + + #[test] + fn test_filter_consecutive_nonzero() { + let records = make_records(&[10, 11, 12]); + assert_eq!(filtered_idxs(&records, 13), vec![10, 11, 12]); + } + + #[test] + fn test_filter_leading_duplicates_before_gap() { + // Walk: 12←11←10, then idx=0 != expected-1=9; first two records dropped. + let records = make_records(&[0, 0, 10, 11, 12]); + assert_eq!(filtered_idxs(&records, 13), vec![10, 11, 12]); + } + + #[test] + fn test_filter_gap_suffix() { + // records [10, 11, 20, 21, 22], next_idx=23 + // → [10, 11] precede a gap at [12..19]; only contiguous suffix [20, 21, 22] survives + let records = make_records(&[10, 11, 20, 21, 22]); + assert_eq!(filtered_idxs(&records, 23), vec![20, 21, 22]); + } + + #[test] + fn test_filter_gap_end() { + // records [10, 11, 20, 21, 22], next_idx=24 + // → last record (idx=22) != next_idx-1 (23), so all records are discarded + let records = make_records(&[10, 11, 20, 21, 22]); + assert_eq!(filtered_idxs(&records, 24), Vec::::new()); + } +} diff --git a/rs/execution_environment/tests/canister_logging.rs b/rs/execution_environment/tests/canister_logging.rs index 2e21a8fa6c3f..a1ec50b73966 100644 --- a/rs/execution_environment/tests/canister_logging.rs +++ b/rs/execution_environment/tests/canister_logging.rs @@ -6,6 +6,7 @@ use ic_config::execution_environment::{ use ic_config::flag_status::FlagStatus; use ic_config::subnet_config::{SubnetConfig, SubnetSecurity}; use ic_execution_environment::units::{KIB, MIB}; +use ic_interfaces_state_manager::{CertificationScope, StateManager}; use ic_management_canister_types_private::{ self as ic00, BoundedAllowedViewers, CanisterIdRecord, CanisterInstallMode, CanisterLogRecord, CanisterSettingsArgs, CanisterSettingsArgsBuilder, DataSize, EmptyBlob, @@ -22,7 +23,7 @@ use ic_test_utilities_execution_environment::{ ExecutionTestBuilder, get_reject, get_reply, wat_canister, wat_fn, }; use ic_test_utilities_metrics::{fetch_histogram_stats, fetch_histogram_vec_stats, labels}; -use ic_types::{CanisterId, NumInstructions, ingress::WasmResult}; +use ic_types::{CanisterId, CanisterLog, NumInstructions, ingress::WasmResult}; use ic_types_cycles::Cycles; use more_asserts::{assert_gt, assert_le, assert_lt}; use proptest::{prelude::ProptestConfig, prop_assume}; @@ -3200,3 +3201,98 @@ fn test_log_memory_store_deallocated_when_canister_out_of_cycles() { .is_allocated() ); } + +#[test] +fn test_log_memory_store_migration_filters_gap_records() { + // Regression test: canister_log records may have gaps in their idx sequence. + // Only the contiguous suffix ending at next_idx - 1 must be migrated. + // + // Records [10, 11, 20, 21, 22] have a gap at [12..19]; with next_idx=23 + // only the contiguous suffix [20, 21, 22] survives. + let controller = PrincipalId::new_anonymous(); + let subnet_type = SubnetType::Application; + + let env = StateMachineBuilder::new() + .with_config(Some(StateMachineConfig::new( + SubnetConfig::new(subnet_type, SubnetSecurity::None), + ExecutionConfig { + log_memory_store_feature: FlagStatus::Disabled, + ..Default::default() + }, + ))) + .with_subnet_type(subnet_type) + .with_checkpoints_enabled(true) + .build(); + + let canister_id = create_and_install_canister( + &env, + CanisterSettingsArgsBuilder::new() + .with_log_visibility(LogVisibilityV2::Public) + .with_controllers(vec![controller]) + .build(), + UNIVERSAL_CANISTER_WASM.to_vec(), + ); + + { + let (_height, mut state) = env.state_manager.take_tip(); + let arc = state.take_canister_state(&canister_id).unwrap(); + let mut c = (*arc).clone(); + // next_idx=23: last record (idx=22) == next_idx-1; gap at [12..19] causes + // [10, 11] to be dropped, leaving contiguous suffix [20, 21, 22]. + c.system_state.canister_log = CanisterLog::new_aggregate( + 23, + vec![ + CanisterLogRecord { + idx: 10, + timestamp_nanos: 1_000, + content: b"a".to_vec(), + }, + CanisterLogRecord { + idx: 11, + timestamp_nanos: 1_001, + content: b"b".to_vec(), + }, + CanisterLogRecord { + idx: 20, + timestamp_nanos: 2_000, + content: b"c".to_vec(), + }, + CanisterLogRecord { + idx: 21, + timestamp_nanos: 2_001, + content: b"d".to_vec(), + }, + CanisterLogRecord { + idx: 22, + timestamp_nanos: 2_002, + content: b"e".to_vec(), + }, + ], + ); + state.put_canister_state(c); + env.state_manager + .commit_and_certify(state, CertificationScope::Full, None); + } + + let env = env.restart_node_with_config(StateMachineConfig::new( + SubnetConfig::new(subnet_type, SubnetSecurity::None), + ExecutionConfig { + log_memory_store_feature: FlagStatus::Enabled, + ..Default::default() + }, + )); + + env.tick(); + + let state = env.get_latest_state(); + let ss = &state.canister_state(&canister_id).unwrap().system_state; + assert!(ss.log_memory_store.is_migrated()); + assert!(ss.log_memory_store.is_allocated()); + assert!(!ss.log_memory_store.is_empty()); + assert_eq!(ss.log_memory_store.next_idx(), 23); + let records = ss.log_memory_store.records(None); + assert_eq!( + records.iter().map(|r| r.idx).collect::>(), + vec![20, 21, 22] + ); +} From 0fbf8c0945d7275267a835f032999d68415a4486 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:17:55 +0200 Subject: [PATCH 37/75] chore(DSM-121): add critical_error_sync_call_unknown_certificate_status (#10466) This PR introduces a critical error for the case of the synchronous update call HTTP handler trying to return a certificate in which the update call status is unknown. This case has not happened in prod for ~10 days by now ([kibana](https://kibana.mainnet.dfinity.network/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-30d%2Fd,to:now))&_a=(columns:!(),filters:!(),hideChart:!f,index:ic-logs-mainnet,interval:auto,query:(language:kuery,query:'%22Unknown%20status%20of%20call%22'),sort:!(!(timestamp,desc))))) so it is safe to assume that the underlying issue has been fixed and raise a critical error going forward. --- rs/http_endpoints/public/src/call/call_sync.rs | 11 +++++++---- rs/http_endpoints/public/src/metrics.rs | 5 +++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/rs/http_endpoints/public/src/call/call_sync.rs b/rs/http_endpoints/public/src/call/call_sync.rs index 9223d9d50e53..f1f657ed4db9 100644 --- a/rs/http_endpoints/public/src/call/call_sync.rs +++ b/rs/http_endpoints/public/src/call/call_sync.rs @@ -8,7 +8,8 @@ use crate::{ HttpError, common::{Cbor, WithTimeout, into_cbor}, metrics::{ - HttpHandlerMetrics, SYNC_CALL_EARLY_RESPONSE_CERTIFICATION_TIMEOUT, + CRITICAL_ERROR_SYNC_CALL_UNKNOWN_CERTIFICATE_STATUS, HttpHandlerMetrics, + SYNC_CALL_EARLY_RESPONSE_CERTIFICATION_TIMEOUT, SYNC_CALL_EARLY_RESPONSE_DUPLICATE_SUBSCRIPTION, SYNC_CALL_EARLY_RESPONSE_INGRESS_WATCHER_NOT_RUNNING, SYNC_CALL_EARLY_RESPONSE_MESSAGE_ALREADY_IN_CERTIFIED_STATE, @@ -345,14 +346,16 @@ async fn call_sync( .with_label_values(&[status_label]) .inc(); - // TODO(DSM-121): ensure that `ParsedMessageStatus::Unknown` never occurs - // and trigger a critical error here if it does. if let ParsedMessageStatus::Unknown = message_status { error!( every_n_seconds => LOG_EVERY_N_SECONDS, log, - "Unknown status of call {} in the certificate at height {}.", message_id, certification.height + "{}: Unknown status of call {} in the certificate at height {}.", + CRITICAL_ERROR_SYNC_CALL_UNKNOWN_CERTIFICATE_STATUS, message_id, certification.height ); + metrics + .critical_error_sync_call_unknown_certificate_status + .inc(); return SyncCallResponse::Accepted( "Certified state does not contain request status. Please try /read_state.", ); diff --git a/rs/http_endpoints/public/src/metrics.rs b/rs/http_endpoints/public/src/metrics.rs index 9ef006db06ea..40263f59226c 100644 --- a/rs/http_endpoints/public/src/metrics.rs +++ b/rs/http_endpoints/public/src/metrics.rs @@ -27,6 +27,8 @@ pub const SYNC_CALL_EARLY_RESPONSE_MESSAGE_ALREADY_IN_CERTIFIED_STATE: &str = "message_already_in_certified_state"; pub const SYNC_CALL_STATUS_IS_NOT_LEAF: &str = "not_leaf"; pub const SYNC_CALL_STATUS_IS_INVALID_UTF8: &str = "is_invalid_utf8"; +pub const CRITICAL_ERROR_SYNC_CALL_UNKNOWN_CERTIFICATE_STATUS: &str = + "http_handler_sync_call_unknown_certificate_status"; /// Placeholder used when we can't determine the appropriate prometheus label. pub const LABEL_UNKNOWN: &str = "unknown"; @@ -71,6 +73,7 @@ pub struct HttpHandlerMetrics { // sync call handler metrics pub sync_call_early_response_trigger_total: IntCounterVec, pub sync_call_certificate_status_total: IntCounterVec, + pub critical_error_sync_call_unknown_certificate_status: IntCounter, // read_state metrics pub read_state_path_type_total: IntCounterVec, @@ -210,6 +213,8 @@ impl HttpHandlerMetrics { "The count of early response triggers for the /{v3,v4}/.../call endpoint.", &[LABEL_SYNC_CALL_EARLY_RESPONSE_TRIGGER], ), + critical_error_sync_call_unknown_certificate_status: metrics_registry + .error_counter(CRITICAL_ERROR_SYNC_CALL_UNKNOWN_CERTIFICATE_STATUS), read_state_path_type_total: metrics_registry.int_counter_vec( "replica_http_read_state_path_type_total", "Count of read_state paths requested, by endpoint type and path type.", From f5557883ab590884daf7ef854a76980991cafc00 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Mon, 15 Jun 2026 12:01:39 +0200 Subject: [PATCH 38/75] fix: deflake //rs/tests/cross_chain:ic_xc_cketh_test by compiling contracts offline (#10459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `//rs/tests/cross_chain:ic_xc_cketh_test` and `…:ic_xc_cketh_test_head_nns` are flaky in data centers with unreliable IPv4 connectivity (e.g. `dm1`), panicking in `deploy_smart_contract`: ``` thread 'main' panicked at rs/tests/cross_chain/ic_xc_cketh_test.rs: called `Result::unwrap()` on an `Err` value: block_on_bash_script: exit_status = 1. Output: Err: Error: error sending request for url (https://binaries.soliditylang.org/linux-amd64/list.json) - Error #1: tcp connect error: Network unreachable (os error 101) ``` ## Root cause The `foundry` container ships `forge`/`cast`/`anvil` but **no `solc`**. When `forge create` compiles a helper contract, it downloads the Solidity compiler from `binaries.soliditylang.org` at test runtime (first the release index `list.json`, then the `solc` binary). That is why the foundry UVM was created with `.enable_ipv4()`. Whenever the UVM's IPv4 connectivity is unavailable, the download fails and the test flakes. This is a third, distinct source of flakiness from the two fixed in #9209. This is also [preventing](https://github.com/dfinity/ic/pull/10253/changes#r3387217110.) these tests to run on the local system-test backend which doesn't have external network access. ## Fix Vendor the required `solc` compilers as pinned, hash-verified Bazel `http_file` dependencies, ship them in the ckETH UVM config image, bind-mount them into the foundry container, and pass them to `forge create --use `. Compilation now happens fully offline and the `.enable_ipv4()` dependency is removed. - **MODULE.bazel**: add the `solc` and `solc_eth_deposit_helper` `http_file` deps (sha256 taken from `list.json`). The compilers are named by role rather than version, so a future compiler bump only touches the `http_file` URL and sha256. - **rs/tests/cross_chain/BUILD.bazel**: add both binaries to the `cketh_uvm_config_image` srcmap (→ `/config/solc` and `/config/solc_eth_deposit_helper`). - **rs/tests/cross_chain/ic_xc_cketh_test.rs**: drop `.enable_ipv4()`; copy the compilers to a writable path + `chmod +x` (vfat drops the exec bit); pass the compiler matching each contract's `pragma` to `forge create --use`. This also removes a source of non-determinism: contracts declaring `pragma solidity ^0.8.20` previously let `svm` pick the latest 0.8.x available at runtime; they are now pinned to exactly 0.8.20. `EthDepositHelper.sol` stays on 0.8.18 to remain faithful to the [deployed mainnet helper contract](https://etherscan.io/address/0x7574eB42cA208A4f6960ECCAfDF186D627dCC175), so it gets its own `solc_eth_deposit_helper` compiler. ## Verification Ran on `dm1` (the flaky environment) with caching disabled: ``` bazel test //rs/tests/cross_chain:ic_xc_cketh_test --cache_test_results=no ``` ``` ============== Summary for //rs/tests/cross_chain:ic_xc_cketh_test ============== Task setup PASSED in 118.22s Task ic_xc_cketh_test PASSED in 225.80s Task assert_no_metrics_errors PASSED in 3.96s Task assert_no_unallowed_log_patterns PASSED in 0.85s //rs/tests/cross_chain:ic_xc_cketh_test PASSED in 353.9s ``` The foundry UVM came up without IPv4, `forge create --use` compiled and deployed all four contracts from the vendored compilers, and every ETH/ERC-20/subaccount deposit flow passed — no network fetch to `binaries.soliditylang.org`. --- MODULE.bazel | 25 +++++++++++++++++++ rs/tests/cross_chain/BUILD.bazel | 2 ++ rs/tests/cross_chain/ic_xc_cketh_test.rs | 31 ++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 41040c5ef31a..4e83a2429eca 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -390,6 +390,31 @@ http_file( url = "https://github.com/dfinity/dogecoin-canister/releases/download/release/2026-03-12/ic-doge-canister.wasm.gz", ) +# solc compilers used by //rs/tests/cross_chain:ic_xc_cketh_test to compile the +# ckETH/ckERC20 helper smart contracts offline. Vendoring these avoids `forge` +# downloading solc from https://binaries.soliditylang.org at test runtime, which +# made the test flaky in data centers with unreliable IPv4 connectivity. +# The sha256 hashes are taken from https://binaries.soliditylang.org/linux-amd64/list.json. +# +# `solc` is used to compile most contracts. `EthDepositHelper.sol` pins an older +# Solidity version to stay faithful to its immutable mainnet deployment, so it +# gets its own `solc_eth_deposit_helper`. +http_file( + name = "solc", + downloaded_file_path = "solc", + executable = True, + sha256 = "0479d44fdf9c501c25337fdc540419f1593b884a87b47f023da4f1c700fda782", + url = "https://binaries.soliditylang.org/linux-amd64/solc-linux-amd64-v0.8.20+commit.a1b79de6", +) + +http_file( + name = "solc_eth_deposit_helper", + downloaded_file_path = "solc_eth_deposit_helper", + executable = True, + sha256 = "95e6ed4949a63ad89afb443ecba1fb8302dd2860ee5e9baace3e674a0f48aa77", + url = "https://binaries.soliditylang.org/linux-amd64/solc-linux-amd64-v0.8.18+commit.87f61d96", +) + # Bitcoin Adapter Mainnet Data for Integration Test # The files have been generated by syncing bitcoind client, followed diff --git a/rs/tests/cross_chain/BUILD.bazel b/rs/tests/cross_chain/BUILD.bazel index 3bcb46926226..b2e143cd5e24 100644 --- a/rs/tests/cross_chain/BUILD.bazel +++ b/rs/tests/cross_chain/BUILD.bazel @@ -80,6 +80,8 @@ uvm_config_image( ":ERC20.sol": "ERC20.sol", ":foundry.tar": "foundry.tar", "//rs/ethereum/cketh/minter:helper_contracts": "/", + "@solc//file": "solc", + "@solc_eth_deposit_helper//file": "solc_eth_deposit_helper", }, tags = ["manual"], # this target will be built if required as a dependency of another target ) diff --git a/rs/tests/cross_chain/ic_xc_cketh_test.rs b/rs/tests/cross_chain/ic_xc_cketh_test.rs index b1c8b016620f..c191757863c4 100644 --- a/rs/tests/cross_chain/ic_xc_cketh_test.rs +++ b/rs/tests/cross_chain/ic_xc_cketh_test.rs @@ -49,6 +49,17 @@ use std::time::Duration; const FOUNDRY_VM_NAME: &str = "foundry"; const DOCKER_NETWORK_NAME: &str = "ethereum"; const FOUNDRY_PORT: u16 = 8545; +/// File names of the `solc` compilers vendored via Bazel (see `MODULE.bazel`) +/// and shipped in the foundry UVM config image. They are used by +/// `deploy_smart_contract` so that `forge` compiles the helper smart contracts +/// offline instead of downloading `solc` at runtime. Each compiler must satisfy +/// the `pragma solidity` of the contracts it compiles. +/// +/// Most contracts are compiled with `SOLC`. `EthDepositHelper.sol` pins an older +/// Solidity version to stay faithful to its immutable mainnet deployment, so it +/// is compiled with its own `SOLC_ETH_DEPOSIT_HELPER`. +const SOLC: &str = "solc"; +const SOLC_ETH_DEPOSIT_HELPER: &str = "solc_eth_deposit_helper"; const ENCODED_PRINCIPAL: &str = "0x1d9facb184cbe453de4841b6b9d9cc95bfc065344e485789b550544529020000"; @@ -86,18 +97,25 @@ fn setup_with_system_and_application_subnets(env: TestEnv) { fn setup_anvil(env: &TestEnv) { UniversalVm::new(String::from(FOUNDRY_VM_NAME)) .with_config_img(get_dependency_path_from_env("CKETH_UVM_CONFIG_PATH")) - .enable_ipv4() //forge needs to download the version of the solidity compiler indicated in the smart contracts that are being deployed .start(env) .expect("failed to setup universal VM"); let deployed_universal_vm = env.get_deployed_universal_vm(FOUNDRY_VM_NAME).unwrap(); + // Copy the vendored `solc` compilers out of the read-only config image into a + // writable location and mark them executable (vfat does not preserve the + // executable bit). `deploy_smart_contract` bind-mounts them into the foundry + // container so that `forge` can compile the helper smart contracts offline + // (the foundry image ships no `solc`). deployed_universal_vm .block_on_bash_script(&format!( r#" docker load -i /config/foundry.tar docker network create {DOCKER_NETWORK_NAME} docker run --net {DOCKER_NETWORK_NAME} --detach --rm --name anvil -p {FOUNDRY_PORT}:{FOUNDRY_PORT} foundry:latest "anvil --host 0.0.0.0" +cp /config/{SOLC_ETH_DEPOSIT_HELPER} /tmp/{SOLC_ETH_DEPOSIT_HELPER} +cp /config/{SOLC} /tmp/{SOLC} +chmod +x /tmp/{SOLC_ETH_DEPOSIT_HELPER} /tmp/{SOLC} "# )) .unwrap(); @@ -384,6 +402,7 @@ fn deploy_eth_deposit_helper_contract( docker_host, &EthereumAccount::HelperContractDeployer, "EthDepositHelper.sol", + SOLC_ETH_DEPOSIT_HELPER, "CkEthDeposit", &minter_address.to_string(), logger, @@ -716,6 +735,7 @@ fn deploy_erc20_helper_contract( docker_host, &EthereumAccount::HelperContractDeployer, "ERC20DepositHelper.sol", + SOLC, "CkErc20Deposit", &minter_address.to_string(), logger, @@ -741,6 +761,7 @@ fn deploy_erc20_contract( foundry, &EthereumAccount::Erc20Deployer, "ERC20.sol", + SOLC, "EXLToken", &format!("0x{initial_supply:x}"), logger, @@ -772,6 +793,7 @@ fn deploy_deposit_with_subaccount_helper_contract( docker_host, &EthereumAccount::HelperContractDeployer, "DepositHelperWithSubaccount.sol", + SOLC, "CkDeposit", &minter_address.to_string(), logger, @@ -814,16 +836,21 @@ fn deploy_smart_contract( foundry: &DeployedUniversalVm, sender: &EthereumAccount, filename: &str, + solc: &str, contract_name: &str, constructor_args: &str, logger: &slog::Logger, ) -> (Address, BlockNumber) { let sender_private_key = sender.private_key(); + // `--use /{solc}` points `forge` at the vendored `solc` binary bind-mounted + // from the UVM (see `setup_anvil`), so compilation happens offline instead of + // `forge` downloading `solc` from the internet. let cmd = format!( "\ docker run --net {DOCKER_NETWORK_NAME} --rm \ -v /config/{filename}:/contracts/{filename} \ - foundry \"forge create --json --rpc-url http://anvil:{FOUNDRY_PORT} --broadcast --private-key {sender_private_key} /contracts/{filename}:{contract_name} --constructor-args {constructor_args}\"\ + -v /tmp/{solc}:/{solc} \ + foundry \"forge create --json --use /{solc} --rpc-url http://anvil:{FOUNDRY_PORT} --broadcast --private-key {sender_private_key} /contracts/{filename}:{contract_name} --constructor-args {constructor_args}\"\ " ); let json_output = foundry.block_on_bash_script(&cmd).unwrap(); From b7a42f92d81ae32e3fb09d43cce1427bb25e9f5d Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Mon, 15 Jun 2026 14:03:56 +0200 Subject: [PATCH 39/75] feat: deduplicate LLVM install (#10471) This streamlines the LLVM install and points the AFL build at the apt-installed LLVM. This drops the llvm 18 install (via clang) and makes the resulting image a bit smaller (2.92GB -> 2.71GB). --------- Co-authored-by: IDX GitHub Automation <> --- .devcontainer/devcontainer.json | 2 +- .github/workflows/api-bn-recovery-test.yml | 2 +- .github/workflows/ci-main.yml | 2 +- .github/workflows/ci-pr-only.yml | 2 +- .github/workflows/container-api-bn-recovery.yml | 2 +- .github/workflows/container-scan-nightly.yml | 2 +- .github/workflows/pocket-ic-tests-windows.yml | 2 +- .github/workflows/rate-limits-backend-release.yml | 2 +- .github/workflows/release-testing.yml | 2 +- .github/workflows/rosetta-release.yml | 2 +- .github/workflows/salt-sharing-canister-release.yml | 2 +- .github/workflows/schedule-daily.yml | 2 +- .github/workflows/schedule-rust-bench.yml | 2 +- .github/workflows/system-tests-benchmarks-nightly.yml | 2 +- .github/workflows/update-mainnet-canister-revisions.yaml | 2 +- ci/container/Dockerfile | 5 ++--- ci/container/TAG | 2 +- ci/container/files/packages.common | 2 +- 18 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c11df7ea7d10..61a711cea9c6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "image": "ghcr.io/dfinity/ic-dev@sha256:92eb89c20b353665d11a5a4e230ff1494c3b017a03ffcb3e8f4136b438165af6", + "image": "ghcr.io/dfinity/ic-dev@sha256:4413ff75554ac7e854fb54715e64641da25a551280d34b4a99c3acd6b5cef6d9", "remoteUser": "ubuntu", "privileged": true, "runArgs": [ diff --git a/.github/workflows/api-bn-recovery-test.yml b/.github/workflows/api-bn-recovery-test.yml index 921b87057d82..c140867dd539 100644 --- a/.github/workflows/api-bn-recovery-test.yml +++ b/.github/workflows/api-bn-recovery-test.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 3d60609d94b0..c21afade622b 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -33,7 +33,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/ci-pr-only.yml b/.github/workflows/ci-pr-only.yml index 9c9ebb41eefd..6b4e4e2cf4c5 100644 --- a/.github/workflows/ci-pr-only.yml +++ b/.github/workflows/ci-pr-only.yml @@ -37,7 +37,7 @@ jobs: runs-on: &dind-small-setup labels: dind-small container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --mount type=tmpfs,target="/tmp/containers" steps: diff --git a/.github/workflows/container-api-bn-recovery.yml b/.github/workflows/container-api-bn-recovery.yml index 03810234ea52..b339b301c172 100644 --- a/.github/workflows/container-api-bn-recovery.yml +++ b/.github/workflows/container-api-bn-recovery.yml @@ -28,7 +28,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/container-scan-nightly.yml b/.github/workflows/container-scan-nightly.yml index fc51d58d6c1f..026876974257 100644 --- a/.github/workflows/container-scan-nightly.yml +++ b/.github/workflows/container-scan-nightly.yml @@ -12,7 +12,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 60 diff --git a/.github/workflows/pocket-ic-tests-windows.yml b/.github/workflows/pocket-ic-tests-windows.yml index 7cc7ce78db22..93d159127c8b 100644 --- a/.github/workflows/pocket-ic-tests-windows.yml +++ b/.github/workflows/pocket-ic-tests-windows.yml @@ -45,7 +45,7 @@ jobs: bazel-build-pocket-ic: name: Bazel Build PocketIC container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/rate-limits-backend-release.yml b/.github/workflows/rate-limits-backend-release.yml index fd4e5c2789c2..c4a74330431e 100644 --- a/.github/workflows/rate-limits-backend-release.yml +++ b/.github/workflows/rate-limits-backend-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/release-testing.yml b/.github/workflows/release-testing.yml index 594fcf9006e1..45b9d74a86ac 100644 --- a/.github/workflows/release-testing.yml +++ b/.github/workflows/release-testing.yml @@ -35,7 +35,7 @@ jobs: group: dm1 labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 180 diff --git a/.github/workflows/rosetta-release.yml b/.github/workflows/rosetta-release.yml index 992ef2040c15..be56b4100079 100644 --- a/.github/workflows/rosetta-release.yml +++ b/.github/workflows/rosetta-release.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" environment: DockerHub diff --git a/.github/workflows/salt-sharing-canister-release.yml b/.github/workflows/salt-sharing-canister-release.yml index 2637efed1ce2..ffe5e25cf214 100644 --- a/.github/workflows/salt-sharing-canister-release.yml +++ b/.github/workflows/salt-sharing-canister-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/schedule-daily.yml b/.github/workflows/schedule-daily.yml index eb7cb7ad2a97..416600982377 100644 --- a/.github/workflows/schedule-daily.yml +++ b/.github/workflows/schedule-daily.yml @@ -14,7 +14,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/schedule-rust-bench.yml b/.github/workflows/schedule-rust-bench.yml index e13a1d9a68c8..65c18cdd917f 100644 --- a/.github/workflows/schedule-rust-bench.yml +++ b/.github/workflows/schedule-rust-bench.yml @@ -24,7 +24,7 @@ jobs: # see linux-x86-64 runner group labels: rust-benchmarks container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 # running on bare metal machine using ubuntu user options: --user ubuntu --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/system-tests-benchmarks-nightly.yml b/.github/workflows/system-tests-benchmarks-nightly.yml index 61943755ef90..692e1b0af597 100644 --- a/.github/workflows/system-tests-benchmarks-nightly.yml +++ b/.github/workflows/system-tests-benchmarks-nightly.yml @@ -17,7 +17,7 @@ jobs: group: dm1 labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 480 diff --git a/.github/workflows/update-mainnet-canister-revisions.yaml b/.github/workflows/update-mainnet-canister-revisions.yaml index 7f487090ec6b..9b06b3c9ad05 100644 --- a/.github/workflows/update-mainnet-canister-revisions.yaml +++ b/.github/workflows/update-mainnet-canister-revisions.yaml @@ -25,7 +25,7 @@ jobs: labels: dind-small environment: CREATE_PR container: - image: ghcr.io/dfinity/ic-build@sha256:55273ed5dc904e737bec678c8e7705aba76cc4b8b039574d5485f1d95d0193d3 + image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" env: diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile index 4a42e7ba0d2a..7a0ffb3d9915 100644 --- a/ci/container/Dockerfile +++ b/ci/container/Dockerfile @@ -68,11 +68,10 @@ RUN curl -fsSL https://github.com/dfinity/motoko/releases/download/${motoko_vers # Install AFLplusplus (`make install` puts the executables in /usr/local/bin/) ARG AFLPLUSPLUS_RELEASE_VERSION=v4.35c -ARG LLVM_VERSION=21 RUN git clone --depth=1 --branch=${AFLPLUSPLUS_RELEASE_VERSION} https://github.com/AFLplusplus/AFLplusplus.git /afl && \ cd /afl && \ - STATIC=1 LLVM_CONFIG=/usr/bin/llvm-config-${LLVM_VERSION} CC=/usr/bin/clang-${LLVM_VERSION} CXX=/usr/bin/clang++-${LLVM_VERSION} make all && \ - STATIC=1 LLVM_CONFIG=/usr/bin/llvm-config-${LLVM_VERSION} CC=/usr/bin/clang-${LLVM_VERSION} CXX=/usr/bin/clang++-${LLVM_VERSION} make install && \ + STATIC=1 LLVM_CONFIG=/usr/bin/llvm-config CC=/usr/bin/clang CXX=/usr/bin/clang++ make all && \ + STATIC=1 LLVM_CONFIG=/usr/bin/llvm-config CC=/usr/bin/clang CXX=/usr/bin/clang++ make install && \ rm -rf /afl # Install rustup proxies system-wide as root: diff --git a/ci/container/TAG b/ci/container/TAG index 2189fe8a9dd8..dcc48239ab5b 100644 --- a/ci/container/TAG +++ b/ci/container/TAG @@ -1 +1 @@ -7e39ead941b80f84a3d65c047ae897bdf2da493dc5a60074e193cee67fbfec8a +aaa0c38e16eef4e2dcca91e82d210a125cbeea4dda546894730b164daaf3b06e diff --git a/ci/container/files/packages.common b/ci/container/files/packages.common index 8abff1144034..c627c83ceff8 100644 --- a/ci/container/files/packages.common +++ b/ci/container/files/packages.common @@ -24,7 +24,7 @@ libboost-dev # RUST gcc pkg-config -libclang-18-dev +libclang-dev libunwind-dev # required for the cargo build of ic-canister-sandbox-backend-lib crate (see: rs/canister_sandbox/src/backtrace.c) libusb-1.0-0-dev # required for HSM lld # required for ovmf_sev genrule From 6e60cad1cc8e3926627f1bf5c66699534d9c6d5a Mon Sep 17 00:00:00 2001 From: "pr-creation-bot-dfinity-ic[bot]" <200595415+pr-creation-bot-dfinity-ic[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:29:09 +0000 Subject: [PATCH 40/75] chore: Update Mainnet IC revisions canisters file (#10473) Update mainnet system canisters revisions file to include the latest WASM version released on the mainnet. This PR is created automatically using [`mainnet_revisions.py`](https://github.com/dfinity/ic/blob/master/ci/src/mainnet_revisions/mainnet_revisions.py) Co-authored-by: CI Automation --- mainnet-canister-revisions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mainnet-canister-revisions.json b/mainnet-canister-revisions.json index 8b042b6c99e1..79fbbe088693 100644 --- a/mainnet-canister-revisions.json +++ b/mainnet-canister-revisions.json @@ -112,8 +112,8 @@ "sha256": "2871c4c885ba201ec83dd5876277af79241ffa591b209663659c114dda8d2d5f" }, "registry": { - "rev": "a0f359b3cb39ec8f3f3f576345ba23cb9133e763", - "sha256": "1415eb5990d283e8ed6b20e732a7d95fec4a1c6b231cf6208c51b603a2c04b42" + "rev": "8facd5635c5e05de9b423b64aeabc2e1ad58d66e", + "sha256": "10029d6fc8c8dd5e75e370e32da22a40453897135903d1d87414fc7af61ba717" }, "root": { "rev": "613e85748f462bc8e169195b16ddcf04706f5d7c", From 24e91285e0b8e7d17698f6c53359df7035266690 Mon Sep 17 00:00:00 2001 From: "pr-creation-bot-dfinity-ic[bot]" <200595415+pr-creation-bot-dfinity-ic[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:38:02 +0000 Subject: [PATCH 41/75] chore: Update Mainnet ICOS revisions file (#10472) Update mainnet revisions file to include the latest version released on the mainnet. This PR is created automatically using [`mainnet_revisions.py`](https://github.com/dfinity/ic/blob/master/ci/src/mainnet_revisions/mainnet_revisions.py) Co-authored-by: CI Automation --- mainnet-icos-revisions.json | 324 +++++++++++++++++++----------------- 1 file changed, 168 insertions(+), 156 deletions(-) diff --git a/mainnet-icos-revisions.json b/mainnet-icos-revisions.json index d4ca6ff5d13b..a24c153a891a 100644 --- a/mainnet-icos-revisions.json +++ b/mainnet-icos-revisions.json @@ -2,45 +2,45 @@ "guestos": { "subnets": { "tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe": { - "version": "a47e5434753752c1d2972fbc4407d14f88964285", - "update_img_hash": "390f4b6c7e3788192be38b970fe62fa12677943a6e281944f4b8a58ec4f461e6", - "update_img_hash_dev": "0ed6a2c0be8b95591ee9fb1d70a1105a3f5e5874942128002b4a4e2393fd8f72", + "version": "6c2b4d16024482f6e9ff794652ab506e603aacc9", + "update_img_hash": "539811f66f243aeea2fb9f4c9adcc129e6c28bf1c4035605e6c9359e50274bbb", + "update_img_hash_dev": "a5f6a32917d53a72bcdb77bb613e5326c3e0be3d5f0e526e507c7b5e8e5e326b", "launch_measurements": { "guest_launch_measurements": [ { - "measurement": [ 107, 32, 139, 57, 115, 42, 23, 108, 94, 228, 237, 173, 203, 68, 176, 7, 93, 142, 200, 168, 127, 50, 229, 156, 118, 48, 13, 18, 195, 30, 223, 93, 99, 173, 65, 126, 133, 205, 55, 108, 87, 127, 253, 90, 15, 104, 246, 92 ], + "measurement": [ 131, 194, 78, 69, 188, 22, 165, 133, 113, 64, 39, 120, 124, 154, 186, 218, 246, 44, 94, 151, 246, 250, 50, 168, 28, 179, 85, 96, 55, 184, 252, 113, 70, 242, 223, 17, 175, 43, 143, 217, 243, 15, 39, 50, 224, 52, 197, 57 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=45248f71c15d6d9605da3924829a86942110976689e54dd1cee5fab3406b6521" } }, { - "measurement": [ 134, 243, 30, 195, 220, 82, 75, 231, 255, 24, 201, 98, 205, 64, 154, 40, 45, 149, 223, 178, 134, 53, 198, 139, 142, 9, 61, 98, 111, 164, 201, 197, 252, 157, 16, 109, 250, 190, 98, 254, 144, 121, 168, 0, 154, 228, 85, 228 ], + "measurement": [ 245, 173, 224, 142, 225, 5, 232, 58, 75, 106, 51, 207, 119, 102, 103, 48, 21, 142, 144, 209, 154, 13, 178, 76, 177, 205, 18, 133, 162, 176, 2, 205, 170, 157, 4, 40, 247, 44, 126, 74, 17, 75, 158, 77, 155, 198, 35, 212 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=45248f71c15d6d9605da3924829a86942110976689e54dd1cee5fab3406b6521" } }, { - "measurement": [ 36, 4, 216, 25, 43, 212, 130, 146, 197, 113, 24, 15, 244, 180, 157, 242, 124, 167, 17, 132, 137, 22, 59, 23, 44, 28, 205, 188, 132, 37, 22, 194, 203, 155, 175, 57, 142, 254, 14, 204, 210, 255, 53, 199, 235, 64, 228, 3 ], + "measurement": [ 227, 238, 247, 246, 60, 123, 107, 182, 244, 55, 228, 171, 43, 233, 38, 177, 208, 238, 63, 120, 79, 139, 149, 66, 226, 158, 197, 135, 43, 45, 188, 29, 178, 148, 211, 71, 71, 145, 254, 230, 188, 41, 169, 176, 227, 179, 224, 38 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=45248f71c15d6d9605da3924829a86942110976689e54dd1cee5fab3406b6521" } }, { - "measurement": [ 22, 183, 43, 124, 65, 130, 159, 27, 133, 75, 133, 10, 188, 53, 113, 208, 212, 186, 107, 152, 152, 223, 114, 143, 23, 36, 63, 195, 209, 133, 250, 68, 83, 156, 36, 232, 18, 83, 48, 200, 49, 251, 37, 8, 75, 239, 153, 48 ], + "measurement": [ 195, 193, 15, 254, 54, 52, 57, 179, 189, 171, 192, 233, 150, 127, 92, 64, 242, 96, 125, 175, 10, 214, 222, 102, 217, 187, 88, 195, 189, 40, 150, 5, 125, 99, 74, 192, 6, 218, 101, 98, 254, 110, 203, 11, 229, 24, 132, 89 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=45248f71c15d6d9605da3924829a86942110976689e54dd1cee5fab3406b6521" } }, { - "measurement": [ 85, 212, 49, 185, 51, 65, 178, 112, 182, 222, 32, 234, 195, 54, 72, 39, 44, 122, 68, 245, 10, 161, 31, 183, 159, 65, 6, 178, 35, 92, 240, 88, 92, 145, 104, 47, 207, 247, 36, 67, 111, 121, 126, 16, 204, 67, 238, 79 ], + "measurement": [ 25, 12, 113, 72, 14, 176, 184, 92, 31, 105, 42, 63, 121, 236, 155, 123, 7, 25, 162, 218, 25, 108, 168, 60, 87, 144, 36, 41, 46, 211, 48, 215, 29, 38, 87, 251, 233, 24, 46, 12, 192, 51, 104, 72, 47, 4, 5, 150 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=45248f71c15d6d9605da3924829a86942110976689e54dd1cee5fab3406b6521" } }, { - "measurement": [ 14, 156, 102, 144, 105, 52, 183, 48, 70, 15, 52, 72, 237, 148, 202, 125, 61, 224, 177, 196, 18, 59, 149, 240, 34, 123, 13, 70, 16, 28, 83, 1, 70, 56, 72, 17, 45, 198, 108, 200, 248, 157, 239, 87, 181, 245, 233, 42 ], + "measurement": [ 192, 42, 158, 238, 249, 71, 77, 84, 94, 226, 93, 200, 95, 219, 244, 151, 138, 112, 73, 89, 27, 236, 53, 226, 16, 10, 8, 133, 57, 9, 218, 66, 0, 234, 137, 235, 162, 252, 219, 120, 70, 22, 149, 146, 41, 111, 153, 119 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b1246b3256df78d54442718c7fba2a572412c23008dd500774486d0de820670a" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=45248f71c15d6d9605da3924829a86942110976689e54dd1cee5fab3406b6521" } } ] @@ -48,120 +48,132 @@ "launch_measurements_dev": { "guest_launch_measurements": [ { - "measurement": [ 75, 75, 167, 109, 233, 98, 201, 171, 9, 233, 185, 47, 23, 162, 73, 46, 63, 149, 103, 126, 193, 21, 145, 54, 181, 129, 72, 51, 131, 93, 197, 190, 143, 130, 132, 242, 177, 183, 16, 86, 119, 122, 184, 48, 135, 200, 186, 50 ], + "measurement": [ 131, 79, 194, 2, 96, 41, 57, 24, 158, 230, 171, 146, 190, 121, 102, 198, 66, 231, 224, 57, 127, 247, 13, 195, 251, 251, 49, 112, 107, 39, 24, 225, 226, 87, 34, 101, 86, 139, 184, 238, 167, 37, 68, 14, 136, 239, 117, 254 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 140, 203, 244, 143, 191, 127, 2, 59, 147, 52, 61, 185, 97, 79, 105, 54, 126, 6, 243, 178, 105, 150, 91, 185, 44, 131, 130, 138, 38, 184, 203, 226, 52, 195, 176, 18, 107, 156, 253, 74, 67, 186, 91, 52, 231, 228, 161, 184 ], + "measurement": [ 190, 201, 170, 2, 27, 214, 69, 40, 65, 86, 135, 60, 116, 202, 82, 17, 179, 111, 241, 232, 9, 235, 202, 25, 243, 27, 88, 15, 42, 104, 1, 107, 241, 163, 26, 186, 156, 244, 25, 183, 52, 99, 102, 119, 158, 149, 45, 241 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 69, 64, 9, 189, 59, 38, 171, 246, 145, 67, 77, 216, 61, 50, 102, 72, 86, 173, 200, 101, 196, 224, 179, 68, 245, 127, 58, 200, 211, 131, 173, 255, 195, 39, 106, 116, 210, 231, 252, 23, 122, 240, 249, 70, 147, 253, 201, 146 ], + "measurement": [ 39, 137, 59, 40, 212, 162, 68, 237, 21, 165, 22, 38, 225, 86, 27, 73, 170, 76, 34, 252, 130, 86, 130, 131, 42, 167, 146, 13, 103, 169, 138, 250, 245, 226, 98, 132, 24, 221, 105, 0, 195, 160, 197, 105, 131, 163, 191, 85 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 31, 113, 210, 32, 92, 227, 224, 247, 145, 123, 20, 15, 53, 235, 40, 235, 8, 190, 124, 211, 70, 27, 68, 181, 222, 113, 235, 213, 83, 101, 208, 142, 216, 188, 72, 68, 118, 80, 205, 44, 153, 52, 129, 154, 93, 130, 220, 70 ], + "measurement": [ 109, 33, 118, 219, 162, 55, 103, 158, 23, 130, 87, 215, 45, 77, 47, 176, 176, 71, 165, 27, 121, 233, 150, 25, 235, 181, 214, 94, 65, 226, 113, 127, 102, 171, 235, 137, 68, 193, 208, 154, 242, 12, 230, 33, 5, 67, 35, 208 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 138, 227, 7, 224, 83, 105, 220, 249, 176, 61, 56, 213, 27, 203, 76, 3, 9, 123, 141, 32, 2, 18, 2, 105, 18, 114, 134, 34, 220, 109, 12, 108, 238, 136, 32, 201, 156, 173, 89, 174, 112, 85, 149, 183, 52, 230, 168, 118 ], + "measurement": [ 159, 109, 18, 96, 127, 179, 253, 69, 188, 184, 159, 239, 118, 241, 59, 140, 236, 255, 208, 49, 150, 235, 195, 200, 140, 55, 183, 10, 249, 142, 25, 1, 227, 220, 90, 61, 74, 79, 61, 160, 71, 7, 170, 196, 74, 172, 234, 204 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 71, 150, 67, 183, 239, 4, 174, 223, 28, 99, 239, 86, 227, 38, 17, 198, 74, 125, 63, 2, 30, 37, 132, 74, 246, 255, 175, 252, 227, 154, 153, 149, 188, 93, 247, 253, 4, 226, 118, 152, 36, 31, 86, 75, 214, 51, 192, 26 ], + "measurement": [ 240, 156, 78, 170, 135, 101, 22, 125, 31, 34, 11, 66, 117, 95, 172, 194, 48, 163, 89, 43, 85, 182, 161, 158, 24, 255, 159, 209, 152, 50, 46, 230, 247, 208, 194, 227, 76, 77, 132, 231, 124, 27, 131, 228, 253, 37, 31, 168 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 14, 51, 47, 86, 244, 91, 201, 89, 152, 73, 46, 198, 106, 13, 238, 85, 48, 228, 229, 180, 66, 99, 241, 164, 204, 30, 49, 82, 234, 160, 193, 155, 118, 137, 151, 110, 119, 226, 64, 56, 94, 136, 135, 54, 232, 51, 12, 253 ], + "measurement": [ 122, 232, 190, 183, 61, 15, 46, 102, 145, 65, 157, 78, 154, 12, 171, 185, 45, 155, 15, 187, 247, 213, 157, 169, 176, 101, 172, 169, 206, 165, 126, 107, 245, 118, 194, 3, 59, 36, 55, 30, 179, 110, 197, 117, 144, 90, 6, 54 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 14, 68, 147, 205, 183, 240, 11, 231, 148, 203, 80, 250, 27, 38, 164, 148, 117, 247, 17, 106, 22, 87, 185, 227, 49, 185, 82, 73, 58, 235, 247, 185, 196, 200, 236, 175, 97, 84, 252, 133, 109, 96, 159, 105, 250, 124, 221, 104 ], + "measurement": [ 86, 17, 151, 208, 47, 174, 207, 146, 147, 77, 24, 55, 194, 231, 223, 181, 134, 179, 67, 218, 15, 159, 124, 252, 232, 127, 130, 134, 103, 118, 151, 42, 50, 85, 134, 117, 220, 209, 228, 48, 79, 98, 60, 231, 188, 3, 63, 99 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 45, 62, 43, 118, 128, 89, 119, 239, 71, 22, 184, 55, 95, 223, 2, 159, 191, 104, 125, 105, 11, 133, 32, 168, 1, 199, 124, 173, 31, 221, 96, 69, 197, 125, 107, 223, 200, 80, 184, 147, 246, 0, 75, 87, 80, 86, 93, 131 ], + "measurement": [ 13, 238, 229, 26, 238, 202, 164, 84, 195, 70, 80, 161, 151, 52, 3, 120, 161, 50, 196, 187, 235, 144, 216, 107, 163, 67, 135, 99, 9, 177, 131, 167, 95, 37, 235, 41, 210, 166, 5, 118, 227, 53, 113, 166, 217, 82, 107, 71 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 42, 139, 145, 0, 203, 82, 212, 146, 64, 141, 162, 164, 126, 1, 80, 80, 249, 252, 83, 189, 79, 77, 201, 196, 69, 199, 56, 213, 77, 60, 72, 236, 56, 83, 220, 168, 130, 37, 251, 15, 164, 233, 1, 105, 200, 110, 156, 139 ], + "measurement": [ 79, 144, 100, 30, 139, 151, 240, 7, 21, 213, 238, 190, 26, 175, 59, 219, 76, 16, 14, 230, 242, 40, 102, 134, 109, 7, 18, 179, 100, 30, 160, 30, 138, 73, 21, 249, 124, 89, 60, 222, 144, 28, 60, 95, 39, 6, 9, 2 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 54, 9, 241, 90, 119, 204, 65, 103, 174, 254, 156, 223, 207, 157, 140, 128, 80, 26, 154, 133, 187, 178, 14, 111, 87, 60, 236, 236, 74, 161, 78, 231, 103, 66, 37, 207, 247, 255, 133, 42, 58, 227, 85, 36, 242, 233, 193, 210 ], + "measurement": [ 99, 117, 74, 66, 56, 151, 248, 202, 221, 132, 52, 21, 126, 171, 88, 30, 145, 5, 213, 100, 62, 144, 35, 99, 113, 80, 191, 96, 30, 201, 184, 233, 135, 10, 251, 172, 123, 127, 33, 145, 57, 76, 226, 142, 125, 112, 157, 152 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 125, 185, 91, 212, 61, 140, 115, 121, 203, 9, 166, 228, 145, 220, 236, 12, 122, 163, 238, 219, 23, 102, 195, 6, 229, 189, 13, 225, 39, 135, 203, 246, 216, 187, 43, 53, 132, 183, 5, 149, 243, 253, 226, 42, 119, 79, 170, 192 ], + "measurement": [ 214, 254, 47, 152, 196, 3, 91, 132, 156, 150, 183, 68, 101, 160, 176, 60, 143, 210, 164, 251, 209, 62, 210, 84, 64, 101, 132, 238, 104, 245, 72, 159, 131, 178, 180, 230, 164, 210, 26, 61, 243, 64, 151, 186, 244, 8, 130, 53 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=b2e25304722c063287878194195830a3ea1d9cd6e0ca911be7dc32c374e8cfc7" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=ed720c345a8e9b1127529be7eb41b49c3d21ea3a79a49fd44cc3de986f27fe14", + "vcpu_type": "EPYC-Turin" } } ] } }, "io67a-2jmkw-zup3h-snbwi-g6a5n-rm5dn-b6png-lvdpl-nqnto-yih6l-gqe": { - "version": "fb721da900b9e9219773ee312f987971338f7c62", - "update_img_hash": "3e6cb724f0cc0a17d1692e91e1f95cfc883c4f6e49146cd57921e69617cedabc", - "update_img_hash_dev": "2ee8c2809aadef08645c453c0388afbb53db410e47665d66a7f214af3334d4e6", + "version": "557d7278dcbb0305411c6536645c3a32b4ec64b6", + "update_img_hash": "5219e12013046d32f512106f7389612eccbeb6ba38eb4bf6391f65504cfa14c9", + "update_img_hash_dev": "752e82a897f5a37456e94503cc5479b9f3c22e81016087583b4e474b836cf646", "launch_measurements": { "guest_launch_measurements": [ { - "measurement": [ 18, 227, 13, 20, 22, 234, 253, 96, 194, 4, 236, 231, 88, 29, 125, 41, 18, 235, 183, 80, 143, 222, 243, 72, 27, 183, 170, 66, 233, 17, 228, 16, 243, 245, 27, 238, 113, 52, 139, 136, 225, 121, 209, 248, 53, 67, 198, 219 ], + "measurement": [ 26, 50, 191, 1, 34, 95, 231, 162, 35, 254, 100, 88, 143, 236, 18, 181, 117, 133, 107, 105, 29, 242, 115, 212, 74, 168, 255, 238, 148, 254, 21, 220, 139, 240, 134, 48, 5, 218, 216, 185, 31, 105, 250, 53, 172, 197, 231, 27 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 168, 20, 35, 37, 80, 160, 194, 73, 121, 117, 175, 87, 158, 43, 227, 86, 23, 93, 118, 104, 10, 43, 72, 8, 251, 95, 53, 210, 107, 57, 159, 101, 251, 163, 160, 61, 240, 8, 82, 78, 93, 109, 19, 238, 10, 147, 125, 128 ], + "measurement": [ 25, 70, 142, 39, 0, 81, 203, 80, 219, 157, 249, 104, 61, 108, 67, 229, 187, 169, 132, 243, 146, 178, 184, 63, 225, 98, 106, 186, 147, 59, 189, 46, 241, 98, 10, 48, 226, 184, 221, 43, 220, 96, 140, 64, 141, 243, 4, 147 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 246, 185, 48, 139, 248, 209, 51, 77, 211, 115, 215, 117, 123, 115, 148, 85, 62, 80, 126, 125, 231, 190, 189, 43, 202, 76, 117, 228, 17, 216, 164, 127, 98, 197, 166, 229, 95, 100, 23, 3, 155, 136, 114, 79, 147, 237, 61, 140 ], + "measurement": [ 168, 205, 69, 126, 64, 61, 215, 148, 113, 7, 249, 49, 151, 193, 238, 126, 218, 55, 15, 0, 102, 157, 26, 13, 143, 145, 227, 11, 162, 89, 226, 199, 39, 13, 82, 206, 186, 125, 0, 3, 89, 206, 229, 22, 246, 212, 200, 94 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 140, 175, 182, 40, 232, 142, 53, 109, 105, 198, 36, 254, 12, 229, 156, 206, 37, 54, 180, 212, 177, 103, 188, 232, 5, 192, 187, 63, 54, 71, 185, 162, 40, 157, 245, 126, 92, 228, 179, 98, 80, 209, 212, 163, 4, 68, 170, 72 ], + "measurement": [ 248, 149, 249, 214, 93, 136, 25, 237, 162, 147, 180, 81, 2, 232, 202, 12, 86, 37, 91, 164, 16, 223, 158, 79, 93, 81, 212, 243, 173, 44, 234, 183, 211, 81, 194, 190, 253, 111, 174, 211, 236, 173, 1, 220, 183, 150, 21, 136 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 127, 58, 219, 103, 155, 159, 173, 5, 157, 169, 196, 5, 3, 123, 239, 211, 19, 224, 219, 31, 211, 124, 22, 206, 210, 232, 7, 66, 155, 60, 10, 47, 175, 183, 125, 165, 202, 58, 51, 113, 147, 128, 84, 165, 83, 152, 197, 26 ], + "measurement": [ 92, 163, 112, 184, 76, 144, 122, 150, 26, 30, 229, 16, 10, 197, 123, 49, 224, 189, 110, 153, 225, 218, 14, 94, 95, 28, 186, 167, 224, 232, 187, 137, 232, 194, 173, 246, 24, 175, 87, 8, 120, 222, 65, 217, 17, 74, 197, 188 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 210, 100, 7, 43, 164, 116, 27, 36, 179, 36, 71, 190, 75, 116, 20, 196, 19, 143, 23, 45, 144, 105, 69, 99, 154, 222, 226, 77, 29, 137, 40, 136, 56, 164, 49, 179, 4, 205, 95, 126, 79, 94, 206, 246, 75, 215, 175, 103 ], + "measurement": [ 26, 17, 90, 2, 200, 6, 80, 69, 120, 233, 155, 252, 126, 32, 221, 236, 150, 192, 57, 249, 47, 59, 2, 45, 187, 72, 202, 202, 142, 227, 245, 157, 62, 178, 64, 87, 154, 60, 230, 208, 27, 96, 90, 23, 11, 188, 112, 105 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } } ] @@ -169,86 +181,86 @@ "launch_measurements_dev": { "guest_launch_measurements": [ { - "measurement": [ 85, 172, 241, 226, 57, 162, 121, 60, 236, 234, 186, 245, 106, 55, 155, 5, 18, 59, 67, 128, 45, 108, 59, 235, 157, 88, 65, 159, 197, 246, 199, 87, 197, 177, 172, 199, 130, 130, 97, 213, 183, 14, 78, 255, 28, 93, 37, 6 ], + "measurement": [ 14, 55, 166, 43, 173, 80, 30, 219, 207, 18, 135, 70, 162, 117, 62, 246, 239, 27, 128, 131, 141, 247, 6, 3, 227, 197, 145, 136, 153, 23, 131, 31, 237, 44, 151, 125, 169, 246, 83, 126, 19, 206, 86, 144, 96, 98, 50, 162 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 167, 190, 44, 164, 33, 11, 140, 155, 83, 189, 161, 253, 157, 69, 71, 100, 245, 16, 21, 9, 116, 142, 112, 80, 205, 152, 90, 248, 194, 135, 190, 123, 191, 135, 240, 208, 183, 188, 120, 168, 180, 210, 131, 47, 149, 71, 222, 90 ], + "measurement": [ 91, 188, 253, 186, 163, 207, 2, 250, 250, 195, 77, 64, 219, 151, 132, 212, 0, 86, 51, 103, 154, 118, 69, 126, 115, 103, 7, 0, 79, 29, 22, 14, 7, 175, 163, 111, 54, 74, 139, 32, 111, 239, 240, 226, 119, 208, 58, 143 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 10, 164, 78, 132, 174, 241, 130, 149, 222, 144, 49, 226, 67, 139, 196, 31, 52, 225, 206, 88, 230, 54, 166, 49, 204, 131, 171, 127, 193, 145, 239, 118, 249, 82, 128, 199, 169, 173, 147, 144, 232, 102, 235, 22, 91, 248, 30, 216 ], + "measurement": [ 232, 68, 59, 125, 222, 131, 133, 153, 40, 167, 111, 122, 218, 81, 232, 156, 160, 19, 187, 176, 242, 169, 227, 120, 168, 179, 96, 52, 152, 107, 145, 123, 184, 151, 7, 165, 182, 46, 112, 195, 160, 16, 150, 227, 92, 19, 219, 254 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 40, 118, 28, 168, 70, 224, 64, 214, 100, 4, 162, 87, 97, 171, 65, 173, 58, 154, 24, 105, 60, 133, 206, 37, 99, 236, 156, 223, 254, 55, 207, 172, 246, 54, 61, 136, 233, 83, 187, 136, 206, 251, 158, 43, 103, 127, 119, 114 ], + "measurement": [ 196, 49, 207, 89, 209, 12, 97, 47, 235, 160, 22, 231, 216, 231, 151, 66, 126, 104, 175, 55, 221, 249, 70, 139, 163, 14, 72, 38, 86, 19, 82, 237, 69, 139, 98, 86, 161, 184, 156, 67, 209, 75, 226, 25, 193, 19, 212, 151 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 126, 219, 193, 137, 242, 209, 198, 170, 251, 179, 170, 55, 84, 99, 45, 69, 28, 109, 239, 20, 132, 86, 175, 43, 161, 44, 37, 246, 50, 160, 212, 202, 242, 157, 250, 244, 146, 44, 16, 170, 110, 83, 69, 129, 139, 81, 116, 30 ], + "measurement": [ 150, 48, 243, 79, 173, 194, 224, 249, 157, 198, 63, 25, 225, 232, 113, 135, 248, 84, 200, 75, 155, 98, 77, 49, 174, 41, 23, 93, 226, 188, 169, 225, 31, 239, 24, 158, 160, 61, 20, 100, 74, 28, 173, 235, 81, 20, 239, 86 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 104, 145, 174, 38, 17, 214, 8, 132, 155, 229, 229, 13, 161, 54, 139, 62, 49, 239, 169, 126, 30, 240, 87, 122, 241, 178, 146, 87, 127, 46, 4, 79, 108, 160, 247, 99, 143, 217, 183, 17, 42, 114, 250, 71, 108, 55, 66, 142 ], + "measurement": [ 123, 158, 67, 13, 161, 18, 115, 246, 21, 58, 160, 227, 126, 51, 74, 97, 107, 171, 120, 20, 119, 86, 88, 212, 255, 23, 88, 206, 221, 60, 118, 230, 158, 79, 45, 225, 58, 32, 213, 161, 64, 138, 89, 180, 235, 204, 83, 191 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 48, 163, 104, 111, 165, 233, 157, 124, 140, 98, 49, 182, 156, 2, 244, 135, 52, 121, 42, 68, 43, 165, 80, 46, 239, 191, 62, 233, 131, 139, 141, 15, 216, 153, 165, 63, 67, 141, 166, 156, 162, 85, 71, 169, 235, 25, 242, 54 ], + "measurement": [ 228, 162, 55, 151, 65, 156, 196, 31, 125, 201, 135, 185, 242, 141, 210, 135, 70, 239, 131, 44, 190, 246, 150, 216, 169, 89, 15, 148, 86, 129, 24, 160, 6, 58, 241, 250, 106, 167, 82, 182, 244, 90, 194, 225, 1, 158, 167, 61 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 95, 93, 149, 160, 171, 205, 254, 68, 115, 167, 69, 26, 159, 71, 175, 4, 73, 36, 58, 29, 11, 215, 190, 201, 66, 197, 170, 174, 77, 177, 85, 235, 191, 71, 194, 151, 190, 211, 164, 49, 183, 138, 152, 239, 123, 87, 83, 240 ], + "measurement": [ 223, 53, 55, 76, 30, 172, 92, 59, 93, 170, 107, 2, 122, 130, 40, 57, 152, 136, 108, 84, 23, 212, 12, 180, 34, 1, 224, 137, 132, 166, 95, 110, 34, 11, 57, 89, 105, 147, 251, 7, 101, 121, 229, 41, 77, 179, 150, 123 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 110, 231, 68, 215, 217, 134, 191, 3, 2, 212, 186, 124, 24, 18, 38, 113, 170, 63, 164, 248, 181, 185, 255, 202, 109, 108, 164, 142, 68, 230, 157, 105, 89, 20, 177, 68, 5, 88, 241, 216, 254, 119, 12, 204, 57, 25, 73, 60 ], + "measurement": [ 1, 179, 164, 197, 194, 254, 225, 205, 162, 101, 171, 197, 104, 184, 89, 36, 73, 187, 150, 238, 26, 33, 83, 193, 49, 140, 14, 162, 154, 203, 159, 35, 17, 106, 131, 155, 252, 63, 191, 197, 121, 41, 106, 236, 51, 197, 27, 21 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 74, 29, 216, 211, 158, 76, 145, 152, 190, 95, 195, 122, 71, 95, 179, 198, 48, 107, 99, 254, 103, 45, 44, 202, 78, 207, 210, 161, 169, 219, 73, 189, 160, 51, 179, 102, 82, 34, 150, 148, 40, 217, 87, 150, 185, 237, 217, 222 ], + "measurement": [ 227, 23, 102, 71, 43, 109, 110, 141, 169, 212, 49, 239, 156, 94, 221, 220, 222, 160, 58, 84, 77, 53, 206, 4, 116, 28, 169, 38, 74, 183, 60, 193, 22, 214, 152, 194, 187, 95, 41, 229, 193, 116, 56, 199, 208, 10, 243, 66 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 62, 218, 131, 254, 38, 89, 53, 214, 14, 33, 42, 222, 124, 44, 190, 126, 26, 155, 214, 243, 77, 106, 226, 43, 64, 50, 203, 58, 201, 176, 253, 77, 100, 0, 47, 179, 104, 35, 226, 157, 93, 29, 150, 15, 7, 7, 58, 226 ], + "measurement": [ 236, 74, 73, 107, 245, 191, 172, 228, 145, 7, 154, 55, 137, 71, 101, 18, 230, 102, 196, 182, 17, 240, 193, 2, 252, 82, 156, 135, 47, 223, 92, 63, 58, 114, 62, 2, 176, 45, 43, 146, 182, 91, 18, 189, 157, 33, 234, 10 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 125, 85, 104, 138, 75, 24, 98, 129, 194, 247, 104, 201, 243, 7, 105, 179, 25, 144, 172, 83, 123, 101, 38, 245, 54, 40, 237, 22, 8, 181, 158, 125, 136, 100, 104, 157, 68, 121, 87, 244, 6, 31, 209, 32, 197, 76, 61, 3 ], + "measurement": [ 204, 87, 127, 213, 52, 161, 162, 47, 6, 114, 138, 148, 195, 85, 1, 58, 204, 153, 183, 41, 192, 72, 106, 61, 38, 241, 11, 144, 43, 69, 71, 170, 170, 12, 183, 12, 201, 226, 168, 195, 75, 221, 89, 214, 224, 42, 12, 15 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } } @@ -257,45 +269,45 @@ } }, "latest_release": { - "version": "b95f4a32b41798de115aac9298b51dd1662f1da5", - "update_img_hash": "82cea02fa3e5ed84a747e91039062c84eab2544f93ee733ccac1212b6d00a5f9", - "update_img_hash_dev": "db3d77485f84d764e49f3d8d4a01d3be7f35dd2d3f2cf6aaf2e6ff562b849050", + "version": "557d7278dcbb0305411c6536645c3a32b4ec64b6", + "update_img_hash": "5219e12013046d32f512106f7389612eccbeb6ba38eb4bf6391f65504cfa14c9", + "update_img_hash_dev": "752e82a897f5a37456e94503cc5479b9f3c22e81016087583b4e474b836cf646", "launch_measurements": { "guest_launch_measurements": [ { - "measurement": [ 34, 114, 220, 159, 105, 224, 48, 120, 210, 74, 82, 29, 214, 84, 15, 166, 45, 172, 164, 250, 179, 225, 244, 89, 185, 116, 2, 207, 232, 168, 59, 143, 183, 107, 162, 92, 151, 78, 145, 92, 186, 187, 203, 26, 192, 241, 206, 209 ], + "measurement": [ 26, 50, 191, 1, 34, 95, 231, 162, 35, 254, 100, 88, 143, 236, 18, 181, 117, 133, 107, 105, 29, 242, 115, 212, 74, 168, 255, 238, 148, 254, 21, 220, 139, 240, 134, 48, 5, 218, 216, 185, 31, 105, 250, 53, 172, 197, 231, 27 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 49, 86, 55, 149, 63, 40, 118, 155, 196, 129, 240, 5, 27, 69, 50, 77, 138, 197, 236, 204, 206, 202, 6, 143, 217, 175, 157, 28, 29, 136, 71, 59, 97, 85, 99, 170, 205, 81, 125, 198, 74, 170, 27, 104, 6, 27, 4, 133 ], + "measurement": [ 25, 70, 142, 39, 0, 81, 203, 80, 219, 157, 249, 104, 61, 108, 67, 229, 187, 169, 132, 243, 146, 178, 184, 63, 225, 98, 106, 186, 147, 59, 189, 46, 241, 98, 10, 48, 226, 184, 221, 43, 220, 96, 140, 64, 141, 243, 4, 147 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 163, 43, 11, 194, 206, 190, 113, 183, 219, 142, 88, 253, 58, 174, 21, 193, 215, 195, 4, 156, 243, 90, 76, 80, 112, 239, 67, 60, 189, 217, 63, 137, 134, 195, 199, 59, 93, 186, 224, 105, 83, 140, 38, 35, 235, 77, 4, 216 ], + "measurement": [ 168, 205, 69, 126, 64, 61, 215, 148, 113, 7, 249, 49, 151, 193, 238, 126, 218, 55, 15, 0, 102, 157, 26, 13, 143, 145, 227, 11, 162, 89, 226, 199, 39, 13, 82, 206, 186, 125, 0, 3, 89, 206, 229, 22, 246, 212, 200, 94 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 39, 188, 204, 153, 129, 177, 232, 4, 188, 190, 54, 178, 9, 201, 187, 190, 220, 85, 11, 239, 6, 107, 73, 62, 94, 150, 254, 48, 112, 144, 145, 5, 204, 180, 177, 60, 205, 121, 62, 48, 84, 70, 131, 152, 85, 237, 195, 245 ], + "measurement": [ 248, 149, 249, 214, 93, 136, 25, 237, 162, 147, 180, 81, 2, 232, 202, 12, 86, 37, 91, 164, 16, 223, 158, 79, 93, 81, 212, 243, 173, 44, 234, 183, 211, 81, 194, 190, 253, 111, 174, 211, 236, 173, 1, 220, 183, 150, 21, 136 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 137, 15, 147, 144, 44, 4, 136, 162, 195, 91, 183, 39, 244, 180, 186, 23, 38, 212, 35, 77, 97, 203, 185, 235, 124, 144, 53, 177, 15, 21, 176, 173, 139, 180, 73, 245, 144, 33, 188, 155, 30, 0, 185, 97, 202, 127, 133, 241 ], + "measurement": [ 92, 163, 112, 184, 76, 144, 122, 150, 26, 30, 229, 16, 10, 197, 123, 49, 224, 189, 110, 153, 225, 218, 14, 94, 95, 28, 186, 167, 224, 232, 187, 137, 232, 194, 173, 246, 24, 175, 87, 8, 120, 222, 65, 217, 17, 74, 197, 188 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 66, 121, 123, 28, 155, 79, 107, 57, 5, 58, 35, 112, 194, 77, 249, 9, 161, 147, 107, 250, 96, 219, 224, 250, 175, 9, 30, 191, 125, 232, 127, 161, 21, 144, 126, 96, 79, 2, 143, 24, 143, 0, 182, 210, 18, 206, 116, 85 ], + "measurement": [ 26, 17, 90, 2, 200, 6, 80, 69, 120, 233, 155, 252, 126, 32, 221, 236, 150, 192, 57, 249, 47, 59, 2, 45, 187, 72, 202, 202, 142, 227, 245, 157, 62, 178, 64, 87, 154, 60, 230, 208, 27, 96, 90, 23, 11, 188, 112, 105 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } } ] @@ -303,86 +315,86 @@ "launch_measurements_dev": { "guest_launch_measurements": [ { - "measurement": [ 45, 160, 192, 227, 159, 190, 252, 225, 41, 170, 227, 50, 254, 228, 50, 215, 114, 29, 28, 53, 96, 26, 140, 173, 71, 148, 215, 164, 102, 144, 188, 218, 155, 21, 184, 224, 179, 35, 97, 253, 142, 243, 175, 232, 250, 123, 121, 129 ], + "measurement": [ 14, 55, 166, 43, 173, 80, 30, 219, 207, 18, 135, 70, 162, 117, 62, 246, 239, 27, 128, 131, 141, 247, 6, 3, 227, 197, 145, 136, 153, 23, 131, 31, 237, 44, 151, 125, 169, 246, 83, 126, 19, 206, 86, 144, 96, 98, 50, 162 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 21, 54, 255, 252, 50, 239, 224, 29, 26, 37, 5, 217, 114, 154, 97, 116, 196, 21, 21, 120, 196, 220, 21, 185, 7, 198, 217, 101, 209, 250, 211, 179, 213, 23, 38, 46, 33, 180, 60, 79, 83, 133, 114, 224, 192, 4, 96, 54 ], + "measurement": [ 91, 188, 253, 186, 163, 207, 2, 250, 250, 195, 77, 64, 219, 151, 132, 212, 0, 86, 51, 103, 154, 118, 69, 126, 115, 103, 7, 0, 79, 29, 22, 14, 7, 175, 163, 111, 54, 74, 139, 32, 111, 239, 240, 226, 119, 208, 58, 143 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 151, 91, 212, 54, 240, 232, 197, 113, 104, 183, 119, 114, 8, 39, 194, 109, 78, 56, 59, 124, 197, 233, 221, 98, 253, 61, 137, 162, 66, 47, 193, 222, 162, 219, 184, 194, 102, 209, 119, 112, 114, 123, 156, 76, 116, 133, 166, 27 ], + "measurement": [ 232, 68, 59, 125, 222, 131, 133, 153, 40, 167, 111, 122, 218, 81, 232, 156, 160, 19, 187, 176, 242, 169, 227, 120, 168, 179, 96, 52, 152, 107, 145, 123, 184, 151, 7, 165, 182, 46, 112, 195, 160, 16, 150, 227, 92, 19, 219, 254 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 225, 77, 76, 133, 146, 38, 38, 114, 130, 169, 26, 12, 251, 140, 60, 162, 23, 6, 168, 230, 13, 83, 40, 246, 105, 63, 123, 204, 183, 153, 83, 247, 163, 217, 83, 57, 48, 209, 247, 165, 223, 151, 10, 217, 129, 11, 29, 237 ], + "measurement": [ 196, 49, 207, 89, 209, 12, 97, 47, 235, 160, 22, 231, 216, 231, 151, 66, 126, 104, 175, 55, 221, 249, 70, 139, 163, 14, 72, 38, 86, 19, 82, 237, 69, 139, 98, 86, 161, 184, 156, 67, 209, 75, 226, 25, 193, 19, 212, 151 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 124, 223, 241, 213, 203, 220, 234, 77, 65, 122, 33, 67, 185, 205, 244, 239, 5, 1, 129, 155, 100, 180, 57, 211, 48, 155, 240, 110, 21, 185, 244, 82, 94, 249, 16, 166, 89, 204, 109, 13, 221, 134, 55, 88, 0, 175, 0, 222 ], + "measurement": [ 150, 48, 243, 79, 173, 194, 224, 249, 157, 198, 63, 25, 225, 232, 113, 135, 248, 84, 200, 75, 155, 98, 77, 49, 174, 41, 23, 93, 226, 188, 169, 225, 31, 239, 24, 158, 160, 61, 20, 100, 74, 28, 173, 235, 81, 20, 239, 86 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 249, 89, 121, 102, 78, 10, 146, 229, 81, 248, 41, 124, 226, 65, 235, 228, 209, 140, 188, 210, 97, 114, 221, 133, 118, 110, 23, 198, 37, 173, 27, 52, 240, 95, 228, 235, 88, 69, 102, 88, 12, 39, 123, 86, 237, 155, 83, 170 ], + "measurement": [ 123, 158, 67, 13, 161, 18, 115, 246, 21, 58, 160, 227, 126, 51, 74, 97, 107, 171, 120, 20, 119, 86, 88, 212, 255, 23, 88, 206, 221, 60, 118, 230, 158, 79, 45, 225, 58, 32, 213, 161, 64, 138, 89, 180, 235, 204, 83, 191 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 168, 73, 2, 8, 252, 197, 99, 17, 56, 1, 199, 118, 60, 171, 71, 115, 168, 42, 16, 33, 13, 92, 113, 109, 181, 128, 219, 110, 155, 58, 229, 251, 243, 148, 71, 22, 19, 12, 60, 0, 220, 229, 25, 116, 220, 140, 253, 183 ], + "measurement": [ 228, 162, 55, 151, 65, 156, 196, 31, 125, 201, 135, 185, 242, 141, 210, 135, 70, 239, 131, 44, 190, 246, 150, 216, 169, 89, 15, 148, 86, 129, 24, 160, 6, 58, 241, 250, 106, 167, 82, 182, 244, 90, 194, 225, 1, 158, 167, 61 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 145, 245, 87, 117, 181, 33, 100, 79, 121, 221, 239, 134, 18, 224, 220, 143, 113, 203, 188, 96, 150, 61, 14, 140, 226, 118, 118, 61, 188, 83, 157, 243, 54, 57, 157, 21, 135, 142, 20, 28, 54, 189, 159, 216, 216, 24, 147, 6 ], + "measurement": [ 223, 53, 55, 76, 30, 172, 92, 59, 93, 170, 107, 2, 122, 130, 40, 57, 152, 136, 108, 84, 23, 212, 12, 180, 34, 1, 224, 137, 132, 166, 95, 110, 34, 11, 57, 89, 105, 147, 251, 7, 101, 121, 229, 41, 77, 179, 150, 123 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 162, 251, 34, 215, 1, 114, 190, 28, 20, 157, 174, 15, 167, 151, 110, 138, 104, 99, 250, 152, 252, 21, 74, 253, 171, 12, 78, 253, 150, 249, 161, 186, 137, 134, 47, 213, 215, 96, 47, 47, 199, 104, 254, 138, 224, 85, 191, 223 ], + "measurement": [ 1, 179, 164, 197, 194, 254, 225, 205, 162, 101, 171, 197, 104, 184, 89, 36, 73, 187, 150, 238, 26, 33, 83, 193, 49, 140, 14, 162, 154, 203, 159, 35, 17, 106, 131, 155, 252, 63, 191, 197, 121, 41, 106, 236, 51, 197, 27, 21 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 96, 124, 167, 148, 64, 107, 158, 31, 231, 155, 134, 132, 191, 120, 205, 210, 71, 17, 139, 154, 97, 104, 205, 219, 72, 175, 34, 42, 21, 232, 247, 135, 242, 113, 218, 23, 51, 4, 194, 84, 187, 9, 129, 128, 100, 77, 228, 218 ], + "measurement": [ 227, 23, 102, 71, 43, 109, 110, 141, 169, 212, 49, 239, 156, 94, 221, 220, 222, 160, 58, 84, 77, 53, 206, 4, 116, 28, 169, 38, 74, 183, 60, 193, 22, 214, 152, 194, 187, 95, 41, 229, 193, 116, 56, 199, 208, 10, 243, 66 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 221, 213, 80, 67, 96, 199, 33, 206, 104, 140, 215, 124, 175, 50, 195, 60, 37, 150, 57, 252, 115, 14, 57, 189, 95, 176, 69, 247, 221, 13, 176, 248, 186, 236, 79, 201, 0, 217, 101, 137, 213, 108, 124, 118, 203, 157, 87, 198 ], + "measurement": [ 236, 74, 73, 107, 245, 191, 172, 228, 145, 7, 154, 55, 137, 71, 101, 18, 230, 102, 196, 182, 17, 240, 193, 2, 252, 82, 156, 135, 47, 223, 92, 63, 58, 114, 62, 2, 176, 45, 43, 146, 182, 91, 18, 189, 157, 33, 234, 10 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 247, 194, 118, 201, 184, 238, 84, 138, 226, 183, 251, 55, 21, 135, 168, 239, 41, 168, 194, 105, 225, 118, 85, 214, 246, 15, 155, 163, 223, 45, 156, 174, 65, 138, 122, 188, 240, 95, 211, 181, 65, 236, 214, 244, 78, 164, 142, 172 ], + "measurement": [ 204, 87, 127, 213, 52, 161, 162, 47, 6, 114, 138, 148, 195, 85, 1, 58, 204, 153, 183, 41, 192, 72, 106, 61, 38, 241, 11, 144, 43, 69, 71, 170, 170, 12, 183, 12, 201, 226, 168, 195, 75, 221, 89, 214, 224, 42, 12, 15 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } } @@ -392,45 +404,45 @@ }, "hostos": { "latest_release": { - "version": "b95f4a32b41798de115aac9298b51dd1662f1da5", - "update_img_hash": "aff602492ef1beabebc66461041dc444ba81f49778b098b7490c9200c6bebecc", - "update_img_hash_dev": "3c734a7fcac9906f352d0c59da799eaa4dc21fabb490c4a1b4917b0f3f8b3302", + "version": "557d7278dcbb0305411c6536645c3a32b4ec64b6", + "update_img_hash": "450469a524e7957237ac9c72d24090488638393b643de3b8aedc687b0d65630f", + "update_img_hash_dev": "c7523b39b9ed8ac7db0f4a6089f7c855aaed0085a51a521a0fdc4e895d6c8aae", "launch_measurements": { "guest_launch_measurements": [ { - "measurement": [ 34, 114, 220, 159, 105, 224, 48, 120, 210, 74, 82, 29, 214, 84, 15, 166, 45, 172, 164, 250, 179, 225, 244, 89, 185, 116, 2, 207, 232, 168, 59, 143, 183, 107, 162, 92, 151, 78, 145, 92, 186, 187, 203, 26, 192, 241, 206, 209 ], + "measurement": [ 26, 50, 191, 1, 34, 95, 231, 162, 35, 254, 100, 88, 143, 236, 18, 181, 117, 133, 107, 105, 29, 242, 115, 212, 74, 168, 255, 238, 148, 254, 21, 220, 139, 240, 134, 48, 5, 218, 216, 185, 31, 105, 250, 53, 172, 197, 231, 27 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 49, 86, 55, 149, 63, 40, 118, 155, 196, 129, 240, 5, 27, 69, 50, 77, 138, 197, 236, 204, 206, 202, 6, 143, 217, 175, 157, 28, 29, 136, 71, 59, 97, 85, 99, 170, 205, 81, 125, 198, 74, 170, 27, 104, 6, 27, 4, 133 ], + "measurement": [ 25, 70, 142, 39, 0, 81, 203, 80, 219, 157, 249, 104, 61, 108, 67, 229, 187, 169, 132, 243, 146, 178, 184, 63, 225, 98, 106, 186, 147, 59, 189, 46, 241, 98, 10, 48, 226, 184, 221, 43, 220, 96, 140, 64, 141, 243, 4, 147 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 163, 43, 11, 194, 206, 190, 113, 183, 219, 142, 88, 253, 58, 174, 21, 193, 215, 195, 4, 156, 243, 90, 76, 80, 112, 239, 67, 60, 189, 217, 63, 137, 134, 195, 199, 59, 93, 186, 224, 105, 83, 140, 38, 35, 235, 77, 4, 216 ], + "measurement": [ 168, 205, 69, 126, 64, 61, 215, 148, 113, 7, 249, 49, 151, 193, 238, 126, 218, 55, 15, 0, 102, 157, 26, 13, 143, 145, 227, 11, 162, 89, 226, 199, 39, 13, 82, 206, 186, 125, 0, 3, 89, 206, 229, 22, 246, 212, 200, 94 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 39, 188, 204, 153, 129, 177, 232, 4, 188, 190, 54, 178, 9, 201, 187, 190, 220, 85, 11, 239, 6, 107, 73, 62, 94, 150, 254, 48, 112, 144, 145, 5, 204, 180, 177, 60, 205, 121, 62, 48, 84, 70, 131, 152, 85, 237, 195, 245 ], + "measurement": [ 248, 149, 249, 214, 93, 136, 25, 237, 162, 147, 180, 81, 2, 232, 202, 12, 86, 37, 91, 164, 16, 223, 158, 79, 93, 81, 212, 243, 173, 44, 234, 183, 211, 81, 194, 190, 253, 111, 174, 211, 236, 173, 1, 220, 183, 150, 21, 136 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 137, 15, 147, 144, 44, 4, 136, 162, 195, 91, 183, 39, 244, 180, 186, 23, 38, 212, 35, 77, 97, 203, 185, 235, 124, 144, 53, 177, 15, 21, 176, 173, 139, 180, 73, 245, 144, 33, 188, 155, 30, 0, 185, 97, 202, 127, 133, 241 ], + "measurement": [ 92, 163, 112, 184, 76, 144, 122, 150, 26, 30, 229, 16, 10, 197, 123, 49, 224, 189, 110, 153, 225, 218, 14, 94, 95, 28, 186, 167, 224, 232, 187, 137, 232, 194, 173, 246, 24, 175, 87, 8, 120, 222, 65, 217, 17, 74, 197, 188 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } }, { - "measurement": [ 66, 121, 123, 28, 155, 79, 107, 57, 5, 58, 35, 112, 194, 77, 249, 9, 161, 147, 107, 250, 96, 219, 224, 250, 175, 9, 30, 191, 125, 232, 127, 161, 21, 144, 126, 96, 79, 2, 143, 24, 143, 0, 182, 210, 18, 206, 116, 85 ], + "measurement": [ 26, 17, 90, 2, 200, 6, 80, 69, 120, 233, 155, 252, 126, 32, 221, 236, 150, 192, 57, 249, 47, 59, 2, 45, 187, 72, 202, 202, 142, 227, 245, 157, 62, 178, 64, 87, 154, 60, 230, 208, 27, 96, 90, 23, 11, 188, 112, 105 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fb8fd147bb6cfb512d727342c24fe70134a9a1cfcc96de3fc7f4d1d6ddb17bed" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" } } ] @@ -438,86 +450,86 @@ "launch_measurements_dev": { "guest_launch_measurements": [ { - "measurement": [ 45, 160, 192, 227, 159, 190, 252, 225, 41, 170, 227, 50, 254, 228, 50, 215, 114, 29, 28, 53, 96, 26, 140, 173, 71, 148, 215, 164, 102, 144, 188, 218, 155, 21, 184, 224, 179, 35, 97, 253, 142, 243, 175, 232, 250, 123, 121, 129 ], + "measurement": [ 14, 55, 166, 43, 173, 80, 30, 219, 207, 18, 135, 70, 162, 117, 62, 246, 239, 27, 128, 131, 141, 247, 6, 3, 227, 197, 145, 136, 153, 23, 131, 31, 237, 44, 151, 125, 169, 246, 83, 126, 19, 206, 86, 144, 96, 98, 50, 162 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 21, 54, 255, 252, 50, 239, 224, 29, 26, 37, 5, 217, 114, 154, 97, 116, 196, 21, 21, 120, 196, 220, 21, 185, 7, 198, 217, 101, 209, 250, 211, 179, 213, 23, 38, 46, 33, 180, 60, 79, 83, 133, 114, 224, 192, 4, 96, 54 ], + "measurement": [ 91, 188, 253, 186, 163, 207, 2, 250, 250, 195, 77, 64, 219, 151, 132, 212, 0, 86, 51, 103, 154, 118, 69, 126, 115, 103, 7, 0, 79, 29, 22, 14, 7, 175, 163, 111, 54, 74, 139, 32, 111, 239, 240, 226, 119, 208, 58, 143 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 151, 91, 212, 54, 240, 232, 197, 113, 104, 183, 119, 114, 8, 39, 194, 109, 78, 56, 59, 124, 197, 233, 221, 98, 253, 61, 137, 162, 66, 47, 193, 222, 162, 219, 184, 194, 102, 209, 119, 112, 114, 123, 156, 76, 116, 133, 166, 27 ], + "measurement": [ 232, 68, 59, 125, 222, 131, 133, 153, 40, 167, 111, 122, 218, 81, 232, 156, 160, 19, 187, 176, 242, 169, 227, 120, 168, 179, 96, 52, 152, 107, 145, 123, 184, 151, 7, 165, 182, 46, 112, 195, 160, 16, 150, 227, 92, 19, 219, 254 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 225, 77, 76, 133, 146, 38, 38, 114, 130, 169, 26, 12, 251, 140, 60, 162, 23, 6, 168, 230, 13, 83, 40, 246, 105, 63, 123, 204, 183, 153, 83, 247, 163, 217, 83, 57, 48, 209, 247, 165, 223, 151, 10, 217, 129, 11, 29, 237 ], + "measurement": [ 196, 49, 207, 89, 209, 12, 97, 47, 235, 160, 22, 231, 216, 231, 151, 66, 126, 104, 175, 55, 221, 249, 70, 139, 163, 14, 72, 38, 86, 19, 82, 237, 69, 139, 98, 86, 161, 184, 156, 67, 209, 75, 226, 25, 193, 19, 212, 151 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 124, 223, 241, 213, 203, 220, 234, 77, 65, 122, 33, 67, 185, 205, 244, 239, 5, 1, 129, 155, 100, 180, 57, 211, 48, 155, 240, 110, 21, 185, 244, 82, 94, 249, 16, 166, 89, 204, 109, 13, 221, 134, 55, 88, 0, 175, 0, 222 ], + "measurement": [ 150, 48, 243, 79, 173, 194, 224, 249, 157, 198, 63, 25, 225, 232, 113, 135, 248, 84, 200, 75, 155, 98, 77, 49, 174, 41, 23, 93, 226, 188, 169, 225, 31, 239, 24, 158, 160, 61, 20, 100, 74, 28, 173, 235, 81, 20, 239, 86 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 249, 89, 121, 102, 78, 10, 146, 229, 81, 248, 41, 124, 226, 65, 235, 228, 209, 140, 188, 210, 97, 114, 221, 133, 118, 110, 23, 198, 37, 173, 27, 52, 240, 95, 228, 235, 88, 69, 102, 88, 12, 39, 123, 86, 237, 155, 83, 170 ], + "measurement": [ 123, 158, 67, 13, 161, 18, 115, 246, 21, 58, 160, 227, 126, 51, 74, 97, 107, 171, 120, 20, 119, 86, 88, 212, 255, 23, 88, 206, 221, 60, 118, 230, 158, 79, 45, 225, 58, 32, 213, 161, 64, 138, 89, 180, 235, 204, 83, 191 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 168, 73, 2, 8, 252, 197, 99, 17, 56, 1, 199, 118, 60, 171, 71, 115, 168, 42, 16, 33, 13, 92, 113, 109, 181, 128, 219, 110, 155, 58, 229, 251, 243, 148, 71, 22, 19, 12, 60, 0, 220, 229, 25, 116, 220, 140, 253, 183 ], + "measurement": [ 228, 162, 55, 151, 65, 156, 196, 31, 125, 201, 135, 185, 242, 141, 210, 135, 70, 239, 131, 44, 190, 246, 150, 216, 169, 89, 15, 148, 86, 129, 24, 160, 6, 58, 241, 250, 106, 167, 82, 182, 244, 90, 194, 225, 1, 158, 167, 61 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 145, 245, 87, 117, 181, 33, 100, 79, 121, 221, 239, 134, 18, 224, 220, 143, 113, 203, 188, 96, 150, 61, 14, 140, 226, 118, 118, 61, 188, 83, 157, 243, 54, 57, 157, 21, 135, 142, 20, 28, 54, 189, 159, 216, 216, 24, 147, 6 ], + "measurement": [ 223, 53, 55, 76, 30, 172, 92, 59, 93, 170, 107, 2, 122, 130, 40, 57, 152, 136, 108, 84, 23, 212, 12, 180, 34, 1, 224, 137, 132, 166, 95, 110, 34, 11, 57, 89, 105, 147, 251, 7, 101, 121, 229, 41, 77, 179, 150, 123 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 162, 251, 34, 215, 1, 114, 190, 28, 20, 157, 174, 15, 167, 151, 110, 138, 104, 99, 250, 152, 252, 21, 74, 253, 171, 12, 78, 253, 150, 249, 161, 186, 137, 134, 47, 213, 215, 96, 47, 47, 199, 104, 254, 138, 224, 85, 191, 223 ], + "measurement": [ 1, 179, 164, 197, 194, 254, 225, 205, 162, 101, 171, 197, 104, 184, 89, 36, 73, 187, 150, 238, 26, 33, 83, 193, 49, 140, 14, 162, 154, 203, 159, 35, 17, 106, 131, 155, 252, 63, 191, 197, 121, 41, 106, 236, 51, 197, 27, 21 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 96, 124, 167, 148, 64, 107, 158, 31, 231, 155, 134, 132, 191, 120, 205, 210, 71, 17, 139, 154, 97, 104, 205, 219, 72, 175, 34, 42, 21, 232, 247, 135, 242, 113, 218, 23, 51, 4, 194, 84, 187, 9, 129, 128, 100, 77, 228, 218 ], + "measurement": [ 227, 23, 102, 71, 43, 109, 110, 141, 169, 212, 49, 239, 156, 94, 221, 220, 222, 160, 58, 84, 77, 53, 206, 4, 116, 28, 169, 38, 74, 183, 60, 193, 22, 214, 152, 194, 187, 95, 41, 229, 193, 116, 56, 199, 208, 10, 243, 66 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 221, 213, 80, 67, 96, 199, 33, 206, 104, 140, 215, 124, 175, 50, 195, 60, 37, 150, 57, 252, 115, 14, 57, 189, 95, 176, 69, 247, 221, 13, 176, 248, 186, 236, 79, 201, 0, 217, 101, 137, 213, 108, 124, 118, 203, 157, 87, 198 ], + "measurement": [ 236, 74, 73, 107, 245, 191, 172, 228, 145, 7, 154, 55, 137, 71, 101, 18, 230, 102, 196, 182, 17, 240, 193, 2, 252, 82, 156, 135, 47, 223, 92, 63, 58, 114, 62, 2, 176, 45, 43, 146, 182, 91, 18, 189, 157, 33, 234, 10 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 247, 194, 118, 201, 184, 238, 84, 138, 226, 183, 251, 55, 21, 135, 168, 239, 41, 168, 194, 105, 225, 118, 85, 214, 246, 15, 155, 163, 223, 45, 156, 174, 65, 138, 122, 188, 240, 95, 211, 181, 65, 236, 214, 244, 78, 164, 142, 172 ], + "measurement": [ 204, 87, 127, 213, 52, 161, 162, 47, 6, 114, 138, 148, 195, 85, 1, 58, 204, 153, 183, 41, 192, 72, 106, 61, 38, 241, 11, 144, 43, 69, 71, 170, 170, 12, 183, 12, 201, 226, 168, 195, 75, 221, 89, 214, 224, 42, 12, 15 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=6909ed2fffe00d1a8e714b4614064c896cb0bae2fe438e7c64770fc519b84f7a", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", "vcpu_type": "EPYC-Turin" } } From c0a8061c8385caa0fa4c05f45414f55e37a45e95 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Mon, 15 Jun 2026 14:57:17 +0200 Subject: [PATCH 42/75] chore: use static IPv6 for GuestOS instead of RA in testnets (#10474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem After [ecf3caf](https://github.com/dfinity/ic/commit/ecf3cafd361d65566339aad729d0d9b0a8dff22b#diff-df1f327e008fa5fb894409aa5d4905e99c5e552dfcb0e924fd326331bdb74328R230) ("feat([NODE-1881](https://dfinity.atlassian.net/browse/NODE-1881)): Discover external IPv6 in the cloud environment (https://github.com/dfinity/ic/pull/9243)"), Farm testnet GuestOS nodes started requesting a **DHCPv4** lease on their `enp1s0` NIC, even though that interface should be IPv6-only. This started exhausting the IPv4 address space in the DCs where we run testnets after 557d727 ("chore(systests): force testnet allocation to the local DC for all system-tests (https://github.com/dfinity/ic/pull/10436)"). ## Root cause The test driver configures every Farm node with `Ipv6Config::RouterAdvertisement`. In the GuestOS network generator (`generate_network_config.rs`), the RA branch emits: ```ini [Network] IPv6AcceptRA=true DHCP=yes ``` In `systemd-networkd`, `DHCP=yes` enables DHCP for **both** IPv4 and IPv6. That line exists to support cloud metadata servers (`169.254.169.254`), but on Farm it makes every node pull a DHCPv4 lease. ## Fix Switch the test driver to build a static `Ipv6Config::Fixed` from the node's already-allocated IPv6 address (available via `node.node_config.public_api`). The gateway is derived as the `::1` address of the node's `/64` subnet — the same derivation already used for SetupOS configs. The `Fixed` branch emits `IPv6AcceptRA=false` and **no** `DHCP=`, so nodes no longer request an IPv4 lease. Cloud/mainnet paths are unaffected: they do not go through this test-driver code and keep using RA + `DHCP=yes`. ## Validation - `rustfmt` + `cargo clippy -p ic-system-test-driver -- -D warnings`: clean - `bazel build //rs/tests/driver:ic-system-test-driver`: success - `bazel test //rs/tests/idx:basic_health_test --cache_test_results=no --test_arg=--set-required-host-features=dc=dm1`: **PASSED in 72.3s** An SSH check on a live Farm node confirmed `::1` resolves to the same physical router (same MAC) as the RA-advertised link-local gateway, and is reachable. [NODE-1881]: https://dfinity.atlassian.net/browse/NODE-1881?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- rs/tests/driver/src/driver/bootstrap.rs | 31 ++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/rs/tests/driver/src/driver/bootstrap.rs b/rs/tests/driver/src/driver/bootstrap.rs index 9feb0c65d5e6..1acdf210a02a 100644 --- a/rs/tests/driver/src/driver/bootstrap.rs +++ b/rs/tests/driver/src/driver/bootstrap.rs @@ -34,9 +34,9 @@ use config_tool::setupos::{ deployment_json::{self, DeploymentSettings}, }; use config_types::{ - CONFIG_VERSION, DeploymentEnvironment, GuestOSConfig, GuestOSDevSettings, GuestOSSettings, - GuestOSUpgradeConfig, GuestVMType, ICOSDevSettings, ICOSSettings, IcBoundaryTlsCert, - Ipv4Config, Ipv6Config, NetworkSettings, RecoveryConfig, + CONFIG_VERSION, DeploymentEnvironment, FixedIpv6Config, GuestOSConfig, GuestOSDevSettings, + GuestOSSettings, GuestOSUpgradeConfig, GuestVMType, ICOSDevSettings, ICOSSettings, + IcBoundaryTlsCert, Ipv4Config, Ipv6Config, NetworkSettings, RecoveryConfig, }; use ic_base_types::NodeId; use ic_prep_lib::{ @@ -56,7 +56,7 @@ use std::{ fs::File, io, io::Write, - net::{IpAddr, SocketAddr}, + net::{IpAddr, Ipv6Addr, SocketAddr}, path::{Path, PathBuf}, process::Command, thread::{self, JoinHandle}, @@ -550,8 +550,27 @@ fn create_guestos_config_for_node( test_env: &TestEnv, ic_name: &str, ) -> anyhow::Result { - // Build NetworkSettings - let ipv6_config = Ipv6Config::RouterAdvertisement; + // Build NetworkSettings. + // + // Use a fixed (static) IPv6 address derived from the node's allocated + // address instead of `RouterAdvertisement`. The RA path makes the GuestOS + // emit `DHCP=yes` (to support cloud metadata servers), which enables DHCP + // for *both* IPv4 and IPv6. On Farm this causes every node to request an + // IPv4 lease and exhausts the IPv4 address space in our testnet DCs. A + // fixed config sets `IPv6AcceptRA=false` and does not enable DHCP. + let ipv6_addr = match node.node_config.public_api.ip() { + IpAddr::V6(addr) => addr, + IpAddr::V4(addr) => bail!("Expected an IPv6 node address, got IPv4: {addr}"), + }; + // The gateway is the `::1` address of the node's /64 subnet. This + // mirrors the gateway derivation used for SetupOS configs and resolves to + // the same physical router that router advertisements point to. + let s = ipv6_addr.segments(); + let ipv6_gateway = Ipv6Addr::new(s[0], s[1], s[2], s[3], 0, 0, 0, 1); + let ipv6_config = Ipv6Config::Fixed(FixedIpv6Config { + address: format!("{ipv6_addr}/64"), + gateway: ipv6_gateway, + }); let ipv4_config = match ipv4_config { Some(config) => Some(Ipv4Config { From a50ec3f47c113a6e19caeafe730c26ae4108dac8 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:21:35 +0200 Subject: [PATCH 43/75] fix: process query stats during idle epochs in PocketIC (#10451) This PR fixes the query stats feature in PocketIC to process empty query stats during idle epochs to unblock processing of (non-empty) stats from previous epochs. The motivation for this change is to provide query stats in local testing (PocketIC) without need to make query calls continuously to have non-empty stats for every epoch (there's no effective change beyond new types in production, i.e., production code paths do not trigger processing of empty query stats). --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Cargo.lock | 1 + rs/execution_environment/src/lib.rs | 11 ++- rs/execution_environment/src/query_handler.rs | 6 +- rs/pocket_ic_server/tests/test.rs | 54 +++++----- rs/protobuf/def/state/stats/v1/stats.proto | 6 ++ rs/protobuf/src/gen/state/state.stats.v1.rs | 9 ++ rs/query_stats/src/payload_builder.rs | 98 ++++++++++++++++++- rs/state_machine_tests/BUILD.bazel | 1 + rs/state_machine_tests/Cargo.toml | 1 + rs/state_machine_tests/src/lib.rs | 5 + .../types/src/batch/execution_environment.rs | 87 ++++++++++++++-- 11 files changed, 236 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16bcb0f104fd..e2c28b4eaa30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14695,6 +14695,7 @@ dependencies = [ "ic-messaging", "ic-metrics", "ic-protobuf", + "ic-query-stats", "ic-registry-client-fake", "ic-registry-client-helpers", "ic-registry-keys", diff --git a/rs/execution_environment/src/lib.rs b/rs/execution_environment/src/lib.rs index 32291d777da9..1aee231f9b81 100644 --- a/rs/execution_environment/src/lib.rs +++ b/rs/execution_environment/src/lib.rs @@ -39,7 +39,7 @@ use ic_interfaces::execution_environment::{ use ic_interfaces_state_manager::StateReader; use ic_logger::ReplicaLogger; use ic_metrics::MetricsRegistry; -use ic_query_stats::QueryStatsPayloadBuilderParams; +use ic_query_stats::{QueryStatsCollector, QueryStatsPayloadBuilderParams}; use ic_registry_subnet_type::SubnetType; use ic_replicated_state::page_map::PageAllocatorFileDescriptor; use ic_replicated_state::{CallOrigin, NetworkTopology, ReplicatedState}; @@ -104,6 +104,7 @@ pub struct ExecutionServices { pub transform_execution_service: TransformExecutionService, pub scheduler: Box>, pub query_stats_payload_builder: QueryStatsPayloadBuilderParams, + pub local_query_execution_stats: Arc, pub cycles_account_manager: Arc, } @@ -130,6 +131,7 @@ impl ExecutionServices { sync_query_handler, query_scheduler, query_stats_payload_builder, + query_stats_collector, cycles_account_manager, execution_environment, ) = setup_execution_helper( @@ -190,6 +192,7 @@ impl ExecutionServices { transform_execution_service, scheduler, query_stats_payload_builder, + local_query_execution_stats: query_stats_collector, cycles_account_manager, } } @@ -254,6 +257,7 @@ impl ExecutionServicesForTesting { sync_query_handler, _query_scheduler, query_stats_payload_builder, + _query_stats_collector, cycles_account_manager, execution_environment, ) = setup_execution_helper( @@ -306,6 +310,7 @@ fn setup_execution_helper( InternalHttpQueryHandler, QueryScheduler, QueryStatsPayloadBuilderParams, + Arc, Arc, Arc, ) { @@ -353,6 +358,7 @@ fn setup_execution_helper( let (query_stats_collector, query_stats_payload_builder) = ic_query_stats::init_query_stats(logger.clone(), &config, metrics_registry); + let query_stats_collector = Arc::new(query_stats_collector); let canister_manager_config: CanisterMgrConfig = CanisterMgrConfig::new( config.default_provisional_cycles_balance, @@ -405,7 +411,7 @@ fn setup_execution_helper( metrics_registry, scheduler_config.max_instructions_per_query_message, Arc::clone(&cycles_account_manager), - query_stats_collector, + Arc::clone(&query_stats_collector), ); let query_scheduler = QueryScheduler::new( @@ -431,6 +437,7 @@ fn setup_execution_helper( sync_query_handler, query_scheduler, query_stats_payload_builder, + query_stats_collector, cycles_account_manager, exec_env, ) diff --git a/rs/execution_environment/src/query_handler.rs b/rs/execution_environment/src/query_handler.rs index 8341aa108a53..c781be5931c3 100644 --- a/rs/execution_environment/src/query_handler.rs +++ b/rs/execution_environment/src/query_handler.rs @@ -119,7 +119,7 @@ pub struct InternalHttpQueryHandler { metrics: QueryHandlerMetrics, max_instructions_per_query: NumInstructions, cycles_account_manager: Arc, - local_query_execution_stats: QueryStatsCollector, + local_query_execution_stats: Arc, query_cache: query_cache::QueryCache, } @@ -133,7 +133,7 @@ impl InternalHttpQueryHandler { metrics_registry: &MetricsRegistry, max_instructions_per_query: NumInstructions, cycles_account_manager: Arc, - local_query_execution_stats: QueryStatsCollector, + local_query_execution_stats: Arc, ) -> Self { let query_cache_capacity = config.query_cache_capacity; let query_max_expiry_time = config.query_cache_max_expiry_time; @@ -344,7 +344,7 @@ impl InternalHttpQueryHandler { let query_stats_collector = if self.config.query_stats_aggregation == FlagStatus::Enabled && enable_query_stats_tracking { - Some(&self.local_query_execution_stats) + Some(self.local_query_execution_stats.as_ref()) } else { None }; diff --git a/rs/pocket_ic_server/tests/test.rs b/rs/pocket_ic_server/tests/test.rs index 9be41ee5ae01..40a3bbaffb4b 100644 --- a/rs/pocket_ic_server/tests/test.rs +++ b/rs/pocket_ic_server/tests/test.rs @@ -938,19 +938,23 @@ fn test_query_stats() { assert_eq!(query_stats.request_payload_bytes_total, zero); assert_eq!(query_stats.response_payload_bytes_total, zero); - // Execute 13 query calls (one per each app subnet node) on the counter canister in each of 4 query stats epochs. + // Execute 13 query calls (one per each app subnet node) on the counter canister in each of the first two query stats epochs. // Every single query call has different arguments so that query calls are not cached. + // Due to a delay in query stats aggregation, two more epochs need to pass before + // we observe the stats from an epoch. let mut n: u64 = 0; - for _ in 0..4 { - for _ in 0..13 { - pic.query_call( - canister_id, - Principal::anonymous(), - "read", - n.to_le_bytes().to_vec(), - ) - .unwrap(); - n += 1; + for i in 0..4 { + if i < 2 { + for _ in 0..13 { + pic.query_call( + canister_id, + Principal::anonymous(), + "read", + n.to_le_bytes().to_vec(), + ) + .unwrap(); + n += 1; + } } // Execute one epoch. for _ in 0..60 { @@ -958,7 +962,7 @@ fn test_query_stats() { } } - // Now the number of calls should be set to 26 (13 calls per epoch from 2 epochs) due to a delay in query stats aggregation. + // Now the number of calls should be set to 26 (13 calls per epoch from 2 epochs). let query_stats = pic.canister_status(canister_id, None).unwrap().query_stats; assert_eq!(query_stats.num_calls_total, candid::Nat::from(26_u64)); assert_ne!(query_stats.num_instructions_total, candid::Nat::from(0_u64)); @@ -1012,28 +1016,28 @@ fn test_query_stats_live() { .query_stats; assert_eq!(query_stats.num_calls_total, candid::Nat::from(0_u64)); - let mut n: u64 = 0; - loop { - // Make one query call per app subnet node. - for _ in 0..13 { - agent - .query(&canister_id, "read") - .with_arg(n.to_le_bytes().to_vec()) - .call() - .await - .unwrap(); - n += 1; - } + // Make one query call per app subnet node to generate stats. + for i in 0_u64..13 { + agent + .query(&canister_id, "read") + .with_arg(i.to_le_bytes().to_vec()) + .call() + .await + .unwrap(); + } + // Wait for query stats to propagate without making additional query calls. + loop { let current_query_stats = ic00 .canister_status(&canister_id) .await .unwrap() .0 .query_stats; - if query_stats.num_calls_total != current_query_stats.num_calls_total { + if current_query_stats.num_calls_total > 0_u64 { break; } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; } }) } diff --git a/rs/protobuf/def/state/stats/v1/stats.proto b/rs/protobuf/def/state/stats/v1/stats.proto index 2509f3d40d8d..2efd6174370e 100644 --- a/rs/protobuf/def/state/stats/v1/stats.proto +++ b/rs/protobuf/def/state/stats/v1/stats.proto @@ -10,6 +10,7 @@ message Stats { message QueryStats { optional uint64 highest_aggregated_epoch = 1; repeated QueryStatsInner query_stats = 2; + repeated EmptyEpochEntry empty_epochs = 3; } message QueryStatsInner { @@ -21,3 +22,8 @@ message QueryStatsInner { uint64 ingress_payload_size = 5; uint64 egress_payload_size = 6; } + +message EmptyEpochEntry { + types.v1.NodeId proposer = 1; + uint64 epoch = 2; +} diff --git a/rs/protobuf/src/gen/state/state.stats.v1.rs b/rs/protobuf/src/gen/state/state.stats.v1.rs index 22dde6976c22..5706a3ea306f 100644 --- a/rs/protobuf/src/gen/state/state.stats.v1.rs +++ b/rs/protobuf/src/gen/state/state.stats.v1.rs @@ -10,6 +10,8 @@ pub struct QueryStats { pub highest_aggregated_epoch: ::core::option::Option, #[prost(message, repeated, tag = "2")] pub query_stats: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "3")] + pub empty_epochs: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct QueryStatsInner { @@ -28,3 +30,10 @@ pub struct QueryStatsInner { #[prost(uint64, tag = "6")] pub egress_payload_size: u64, } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EmptyEpochEntry { + #[prost(message, optional, tag = "1")] + pub proposer: ::core::option::Option, + #[prost(uint64, tag = "2")] + pub epoch: u64, +} diff --git a/rs/query_stats/src/payload_builder.rs b/rs/query_stats/src/payload_builder.rs index e32a04eb636d..f1abd4da13d0 100644 --- a/rs/query_stats/src/payload_builder.rs +++ b/rs/query_stats/src/payload_builder.rs @@ -182,7 +182,7 @@ impl QueryStatsPayloadBuilderImpl { return vec![]; } - let Ok(previous_ids) = + let Ok((previous_ids, has_submitted)) = self.get_previous_ids(self.node_id, current_stats.epoch, past_payloads, context) else { return vec![]; @@ -196,7 +196,10 @@ impl QueryStatsPayloadBuilderImpl { .cloned() .collect::>(); - if messages.is_empty() { + // Skip if there is nothing new to report and we've already submitted for this epoch. + // If we have not yet submitted for this epoch, fall through to send an empty payload + // so the aggregator can see that this node has moved past this epoch. + if messages.is_empty() && has_submitted { return vec![]; } @@ -273,7 +276,7 @@ impl QueryStatsPayloadBuilderImpl { // Get the previous ids, that have been already reported by this node in the epoch // NOTE: This also checks that the epoch that is being reported has not been aggregated yet - let previous_ids = self.get_previous_ids( + let (previous_ids, _) = self.get_previous_ids( payload.proposer, payload.epoch, past_payloads, @@ -304,13 +307,16 @@ impl QueryStatsPayloadBuilderImpl { /// /// This function also returns an error, if we are requesting data on an epoch, /// that has been already aggregated. + /// + /// Returns `(previous_canister_ids, has_submitted)` where `has_submitted` is true if this + /// node has already submitted any report (even empty) for the given epoch. fn get_previous_ids( &self, node_id: NodeId, epoch: QueryStatsEpoch, past_payloads: &[PastPayload], context: &ValidationContext, - ) -> Result, PayloadValidationError> { + ) -> Result<(BTreeSet, bool), PayloadValidationError> { // Get unaggregated stats from certified state let certified_height = context.certified_height; let state_stats = &match self.state_reader.get_state_at(certified_height) { @@ -363,13 +369,16 @@ impl QueryStatsPayloadBuilderImpl { } // Check the certified state for stats that we have already sent + let mut has_submitted_in_state = false; if let Some(state_stats) = get_stats_for_node_id_and_epoch(state_stats, &node_id, &epoch) + .inspect(|_| has_submitted_in_state = true) .map(|record| record.keys()) { previous_ids.extend(state_stats); } // Check past payloads for stats already sent + let mut has_submitted_in_past = false; previous_ids.extend( past_payloads .iter() @@ -387,6 +396,10 @@ impl QueryStatsPayloadBuilderImpl { }) // Filter out payloads that have a different epoch or are sent from different node .filter(|stats| stats.epoch == epoch && stats.proposer == node_id) + // Track whether this node has submitted anything for this epoch + .inspect(|_| { + has_submitted_in_past = true; + }) // Map payload to CanisterIds .flat_map(|stats| { stats @@ -397,7 +410,10 @@ impl QueryStatsPayloadBuilderImpl { }), ); - Ok(previous_ids) + Ok(( + previous_ids, + has_submitted_in_state || has_submitted_in_past, + )) } } @@ -756,6 +772,78 @@ mod tests { } } + /// When current stats are empty and the node has not yet submitted for the epoch, + /// the builder must emit a non-empty metadata-only payload so the aggregator can + /// see that this node has moved past the epoch. + #[test] + fn idle_epoch_first_build_returns_metadata_only_payload() { + let test_stats = test_epoch_stats(0, 0); + let state = test_state(RawQueryStats::default()); + let payload_builder = setup_payload_builder_impl(state, test_stats); + let validation_context = test_validation_context(); + + let payload = payload_builder.build_payload_impl( + Height::new(1), + MAX_PAYLOAD_SIZE, + &[], + &validation_context, + ); + + assert!(!payload.is_empty()); + let deserialized = QueryStatsPayload::deserialize(&payload).unwrap().unwrap(); + assert_eq!(deserialized.epoch, QueryStatsEpoch::new(0)); + assert_eq!(deserialized.proposer, node_test_id(1)); + assert!(deserialized.stats.is_empty()); + } + + /// Once an empty-stats payload from a prior block is in past_payloads, subsequent + /// builds must return vec![] to avoid sending duplicate epoch-advancement signals. + #[test] + fn idle_epoch_subsequent_build_returns_empty_after_past_payload() { + let test_stats = test_epoch_stats(0, 0); + let state = test_state(RawQueryStats::default()); + let payload_builder = setup_payload_builder_impl(state, test_stats.clone()); + let validation_context = test_validation_context(); + + let prior_empty_payload = payload_from_range(&test_stats, 0..0, node_test_id(1)); + let prior_past_payload = as_past_payload(&prior_empty_payload, 1); + + let payload = payload_builder.build_payload_impl( + Height::new(2), + MAX_PAYLOAD_SIZE, + &[prior_past_payload], + &validation_context, + ); + + assert!(payload.is_empty()); + } + + /// Once an empty-epoch sentinel is recorded in replicated state, subsequent builds + /// must also return vec![] (the has_submitted_in_state path). + #[test] + fn idle_epoch_subsequent_build_returns_empty_after_state_submission() { + let test_stats = test_epoch_stats(0, 0); + // epoch_stats_for_state with an empty range inserts node -> epoch -> {} into + // state, which is the sentinel for a prior empty submission. + let state = test_state(epoch_stats_for_state( + &test_stats, + 0..0, + node_test_id(1), + None, + )); + let payload_builder = setup_payload_builder_impl(state, test_stats); + let validation_context = test_validation_context(); + + let payload = payload_builder.build_payload_impl( + Height::new(1), + MAX_PAYLOAD_SIZE, + &[], + &validation_context, + ); + + assert!(payload.is_empty()); + } + fn test_validation_context() -> ValidationContext { ValidationContext { registry_version: RegistryVersion::new(0), diff --git a/rs/state_machine_tests/BUILD.bazel b/rs/state_machine_tests/BUILD.bazel index c372cb06bdf6..fa6c48498ea1 100644 --- a/rs/state_machine_tests/BUILD.bazel +++ b/rs/state_machine_tests/BUILD.bazel @@ -42,6 +42,7 @@ rust_library( "//rs/monitoring/logger", "//rs/monitoring/metrics", "//rs/protobuf", + "//rs/query_stats", "//rs/registry/fake", "//rs/registry/helpers", "//rs/registry/keys", diff --git a/rs/state_machine_tests/Cargo.toml b/rs/state_machine_tests/Cargo.toml index 0aba1ce2d9a8..e32f7333b3a0 100644 --- a/rs/state_machine_tests/Cargo.toml +++ b/rs/state_machine_tests/Cargo.toml @@ -44,6 +44,7 @@ ic-management-canister-types-private = { path = "../types/management_canister_ty ic-messaging = { path = "../messaging" } ic-metrics = { path = "../monitoring/metrics" } ic-protobuf = { path = "../protobuf" } +ic-query-stats = { path = "../query_stats" } ic-registry-client-fake = { path = "../registry/fake" } ic-registry-client-helpers = { path = "../registry/helpers" } ic-registry-keys = { path = "../registry/keys" } diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 2e4bd98268e5..29187c2ff0f6 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -92,6 +92,7 @@ use ic_protobuf::{ v1::{PrincipalId as PrincipalIdIdProto, SubnetId as SubnetIdProto}, }, }; +use ic_query_stats::QueryStatsCollector; use ic_registry_client_fake::FakeRegistryClient; use ic_registry_client_helpers::{ provisional_whitelist::ProvisionalWhitelistRegistry, @@ -1249,6 +1250,7 @@ pub struct StateMachine { /// A drop guard to gracefully cancel the ingress watcher task. _ingress_watcher_drop_guard: tokio_util::sync::DropGuard, query_stats_payload_builder: Arc, + local_query_execution_stats: Arc, chain_key_payload_builder: Arc, remove_old_states: bool, cycles_account_manager: Arc, @@ -2000,6 +2002,8 @@ impl StateMachine { // Finally execute the payload. self.execute_payload(payload); + self.local_query_execution_stats + .set_epoch_from_height(self.state_manager.latest_state_height()); } /// Reload registry derived from a *shared* registry data provider @@ -2434,6 +2438,7 @@ impl StateMachine { canister_http_pool, canister_http_payload_builder, query_stats_payload_builder: pocket_query_stats_payload_builder, + local_query_execution_stats: execution_services.local_query_execution_stats, chain_key_payload_builder, remove_old_states, cycles_account_manager: execution_services.cycles_account_manager, diff --git a/rs/types/types/src/batch/execution_environment.rs b/rs/types/types/src/batch/execution_environment.rs index e58b22ffc349..543a4007ac70 100644 --- a/rs/types/types/src/batch/execution_environment.rs +++ b/rs/types/types/src/batch/execution_environment.rs @@ -28,7 +28,7 @@ use ic_protobuf::{ proxy::{ProxyDecodeError, try_from_option_field}, state::{ canister_state_bits::v1::{TotalQueryStats as TotalQueryStatsProto, Unsigned128}, - stats::v1::{QueryStats as QueryStatsProto, QueryStatsInner}, + stats::v1::{EmptyEpochEntry, QueryStats as QueryStatsProto, QueryStatsInner}, }, types::v1::{self as pb}, }; @@ -141,11 +141,17 @@ pub struct RawQueryStats { impl RawQueryStats { pub fn as_query_stats(&self) -> Option { - // Serialize BTreeMap as vector let mut query_stats = vec![]; + let mut empty_epochs = vec![]; for (node_id, inner) in &self.stats { for (epoch, inner) in inner { + if inner.is_empty() { + empty_epochs.push(EmptyEpochEntry { + proposer: Some(node_id_into_protobuf(*node_id)), + epoch: epoch.get(), + }); + } for (canister_id, stats) in inner { query_stats.push(QueryStatsInner { proposer: Some(node_id_into_protobuf(*node_id)), @@ -160,12 +166,16 @@ impl RawQueryStats { } } - if query_stats.is_empty() && self.highest_aggregated_epoch.is_none() { + if query_stats.is_empty() + && empty_epochs.is_empty() + && self.highest_aggregated_epoch.is_none() + { None } else { Some(QueryStatsProto { highest_aggregated_epoch: self.highest_aggregated_epoch.map(|epoch| epoch.get()), query_stats, + empty_epochs, }) } } @@ -184,7 +194,6 @@ impl TryFrom for RawQueryStats { let epoch = QueryStatsEpoch::new(entry.epoch); let canister: CanisterId = try_from_option_field(entry.canister, "QueryStatsInner::canister_id")?; - r.stats .entry(proposer) .or_default() @@ -201,6 +210,16 @@ impl TryFrom for RawQueryStats { ); } } + for entry in value.empty_epochs { + if let Ok(proposer) = node_id_try_from_option(entry.proposer) { + let epoch = QueryStatsEpoch::new(entry.epoch); + r.stats + .entry(proposer) + .or_default() + .entry(epoch) + .or_default(); + } + } Ok(r) } } @@ -224,10 +243,6 @@ impl QueryStatsPayload { /// This function will drop trailing stats to guarantee, that the /// payload will fit into the `byte_limit` pub fn serialize_with_limit(&self, byte_limit: NumBytes) -> Vec { - if self.stats.is_empty() { - return vec![]; - } - let mut buffer = vec![].limit(byte_limit.get() as usize); // Encode the metadata about the messages @@ -242,6 +257,11 @@ impl QueryStatsPayload { Err(_) => return vec![], } + // If there are no stats, return just epoch+proposer to signal epoch advancement. + if self.stats.is_empty() { + return buffer.into_inner(); + } + let mut num_stats_included = 0; for entry in &self.stats { if pb::CanisterQueryStats::from(entry) @@ -382,6 +402,28 @@ mod tests { assert!(original_stats.stats.len() > deserialized_stats.stats.len()); } + /// Empty stats serialize to a metadata-only payload that round-trips correctly. + #[test] + fn empty_stats_metadata_only_roundtrip() { + let epoch = QueryStatsEpoch::new(42); + let proposer = NodeId::from(PrincipalId::new_node_test_id(7)); + let payload = QueryStatsPayload { + epoch, + proposer, + stats: vec![], + }; + + let serialized = payload.serialize_with_limit(NumBytes::new(2 * 1024 * 1024)); + assert!(!serialized.is_empty()); + + let deserialized = QueryStatsPayload::deserialize(&serialized) + .unwrap() + .unwrap(); + assert_eq!(deserialized.epoch, epoch); + assert_eq!(deserialized.proposer, proposer); + assert!(deserialized.stats.is_empty()); + } + fn test_message(num_stats: u64) -> QueryStatsPayload { let mut rng = ChaCha8Rng::seed_from_u64(1454); @@ -420,6 +462,35 @@ mod tests { assert_eq!(test, check_test); } + /// An epoch entry with an empty canister map must survive the as_query_stats() -> try_from() roundtrip. + #[test] + fn serialization_roundtrip_raw_query_stats_empty_epoch() { + // One node with two epochs: one that has canister stats and one that is empty. + let mut rng = ChaCha8Rng::seed_from_u64(1454); + + let mut record = BTreeMap::new(); + let mut inner = BTreeMap::new(); + inner.insert(canister_test_id(1), rng_epoch_stats(&mut rng)); + record.insert(QueryStatsEpoch::new(0), inner); + // Empty canister map — the sentinel path. + record.insert(QueryStatsEpoch::new(1), BTreeMap::new()); + + let mut stats = BTreeMap::new(); + stats.insert(node_test_id(1), record); + + let test = RawQueryStats { + highest_aggregated_epoch: None, + stats, + }; + + let pb_test = test.as_query_stats().unwrap(); + let check_test = RawQueryStats::try_from(pb_test).unwrap(); + + assert_eq!(test, check_test); + // The empty epoch must be preserved as an empty map, not absent. + assert!(check_test.stats[&node_test_id(1)][&QueryStatsEpoch::new(1)].is_empty()); + } + fn rng_epoch_stats(rng: &mut R) -> QueryStats where R: RngCore, From f0a032e3957b33ed1ec4936d80171b10b5a7e213 Mon Sep 17 00:00:00 2001 From: "pr-creation-bot-dfinity-ic[bot]" <200595415+pr-creation-bot-dfinity-ic[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:56:20 +0000 Subject: [PATCH 44/75] chore: Update Mainnet ICOS revisions file (#10477) Update mainnet revisions file to include the latest version released on the mainnet. This PR is created automatically using [`mainnet_revisions.py`](https://github.com/dfinity/ic/blob/master/ci/src/mainnet_revisions/mainnet_revisions.py) Co-authored-by: CI Automation --- mainnet-icos-revisions.json | 78 ++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/mainnet-icos-revisions.json b/mainnet-icos-revisions.json index a24c153a891a..eb60da5e1d5f 100644 --- a/mainnet-icos-revisions.json +++ b/mainnet-icos-revisions.json @@ -135,45 +135,45 @@ } }, "io67a-2jmkw-zup3h-snbwi-g6a5n-rm5dn-b6png-lvdpl-nqnto-yih6l-gqe": { - "version": "557d7278dcbb0305411c6536645c3a32b4ec64b6", - "update_img_hash": "5219e12013046d32f512106f7389612eccbeb6ba38eb4bf6391f65504cfa14c9", - "update_img_hash_dev": "752e82a897f5a37456e94503cc5479b9f3c22e81016087583b4e474b836cf646", + "version": "fb721da900b9e9219773ee312f987971338f7c62", + "update_img_hash": "3e6cb724f0cc0a17d1692e91e1f95cfc883c4f6e49146cd57921e69617cedabc", + "update_img_hash_dev": "2ee8c2809aadef08645c453c0388afbb53db410e47665d66a7f214af3334d4e6", "launch_measurements": { "guest_launch_measurements": [ { - "measurement": [ 26, 50, 191, 1, 34, 95, 231, 162, 35, 254, 100, 88, 143, 236, 18, 181, 117, 133, 107, 105, 29, 242, 115, 212, 74, 168, 255, 238, 148, 254, 21, 220, 139, 240, 134, 48, 5, 218, 216, 185, 31, 105, 250, 53, 172, 197, 231, 27 ], + "measurement": [ 18, 227, 13, 20, 22, 234, 253, 96, 194, 4, 236, 231, 88, 29, 125, 41, 18, 235, 183, 80, 143, 222, 243, 72, 27, 183, 170, 66, 233, 17, 228, 16, 243, 245, 27, 238, 113, 52, 139, 136, 225, 121, 209, 248, 53, 67, 198, 219 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 25, 70, 142, 39, 0, 81, 203, 80, 219, 157, 249, 104, 61, 108, 67, 229, 187, 169, 132, 243, 146, 178, 184, 63, 225, 98, 106, 186, 147, 59, 189, 46, 241, 98, 10, 48, 226, 184, 221, 43, 220, 96, 140, 64, 141, 243, 4, 147 ], + "measurement": [ 168, 20, 35, 37, 80, 160, 194, 73, 121, 117, 175, 87, 158, 43, 227, 86, 23, 93, 118, 104, 10, 43, 72, 8, 251, 95, 53, 210, 107, 57, 159, 101, 251, 163, 160, 61, 240, 8, 82, 78, 93, 109, 19, 238, 10, 147, 125, 128 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 168, 205, 69, 126, 64, 61, 215, 148, 113, 7, 249, 49, 151, 193, 238, 126, 218, 55, 15, 0, 102, 157, 26, 13, 143, 145, 227, 11, 162, 89, 226, 199, 39, 13, 82, 206, 186, 125, 0, 3, 89, 206, 229, 22, 246, 212, 200, 94 ], + "measurement": [ 246, 185, 48, 139, 248, 209, 51, 77, 211, 115, 215, 117, 123, 115, 148, 85, 62, 80, 126, 125, 231, 190, 189, 43, 202, 76, 117, 228, 17, 216, 164, 127, 98, 197, 166, 229, 95, 100, 23, 3, 155, 136, 114, 79, 147, 237, 61, 140 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 248, 149, 249, 214, 93, 136, 25, 237, 162, 147, 180, 81, 2, 232, 202, 12, 86, 37, 91, 164, 16, 223, 158, 79, 93, 81, 212, 243, 173, 44, 234, 183, 211, 81, 194, 190, 253, 111, 174, 211, 236, 173, 1, 220, 183, 150, 21, 136 ], + "measurement": [ 140, 175, 182, 40, 232, 142, 53, 109, 105, 198, 36, 254, 12, 229, 156, 206, 37, 54, 180, 212, 177, 103, 188, 232, 5, 192, 187, 63, 54, 71, 185, 162, 40, 157, 245, 126, 92, 228, 179, 98, 80, 209, 212, 163, 4, 68, 170, 72 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 92, 163, 112, 184, 76, 144, 122, 150, 26, 30, 229, 16, 10, 197, 123, 49, 224, 189, 110, 153, 225, 218, 14, 94, 95, 28, 186, 167, 224, 232, 187, 137, 232, 194, 173, 246, 24, 175, 87, 8, 120, 222, 65, 217, 17, 74, 197, 188 ], + "measurement": [ 127, 58, 219, 103, 155, 159, 173, 5, 157, 169, 196, 5, 3, 123, 239, 211, 19, 224, 219, 31, 211, 124, 22, 206, 210, 232, 7, 66, 155, 60, 10, 47, 175, 183, 125, 165, 202, 58, 51, 113, 147, 128, 84, 165, 83, 152, 197, 26 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } }, { - "measurement": [ 26, 17, 90, 2, 200, 6, 80, 69, 120, 233, 155, 252, 126, 32, 221, 236, 150, 192, 57, 249, 47, 59, 2, 45, 187, 72, 202, 202, 142, 227, 245, 157, 62, 178, 64, 87, 154, 60, 230, 208, 27, 96, 90, 23, 11, 188, 112, 105 ], + "measurement": [ 210, 100, 7, 43, 164, 116, 27, 36, 179, 36, 71, 190, 75, 116, 20, 196, 19, 143, 23, 45, 144, 105, 69, 99, 154, 222, 226, 77, 29, 137, 40, 136, 56, 164, 49, 179, 4, 205, 95, 126, 79, 94, 206, 246, 75, 215, 175, 103 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=50210f92bdc5eb17039f61e990c9f0ad8af92712406155d8788f91ab4faaeb95" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" } } ] @@ -181,86 +181,86 @@ "launch_measurements_dev": { "guest_launch_measurements": [ { - "measurement": [ 14, 55, 166, 43, 173, 80, 30, 219, 207, 18, 135, 70, 162, 117, 62, 246, 239, 27, 128, 131, 141, 247, 6, 3, 227, 197, 145, 136, 153, 23, 131, 31, 237, 44, 151, 125, 169, 246, 83, 126, 19, 206, 86, 144, 96, 98, 50, 162 ], + "measurement": [ 85, 172, 241, 226, 57, 162, 121, 60, 236, 234, 186, 245, 106, 55, 155, 5, 18, 59, 67, 128, 45, 108, 59, 235, 157, 88, 65, 159, 197, 246, 199, 87, 197, 177, 172, 199, 130, 130, 97, 213, 183, 14, 78, 255, 28, 93, 37, 6 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 91, 188, 253, 186, 163, 207, 2, 250, 250, 195, 77, 64, 219, 151, 132, 212, 0, 86, 51, 103, 154, 118, 69, 126, 115, 103, 7, 0, 79, 29, 22, 14, 7, 175, 163, 111, 54, 74, 139, 32, 111, 239, 240, 226, 119, 208, 58, 143 ], + "measurement": [ 167, 190, 44, 164, 33, 11, 140, 155, 83, 189, 161, 253, 157, 69, 71, 100, 245, 16, 21, 9, 116, 142, 112, 80, 205, 152, 90, 248, 194, 135, 190, 123, 191, 135, 240, 208, 183, 188, 120, 168, 180, 210, 131, 47, 149, 71, 222, 90 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 232, 68, 59, 125, 222, 131, 133, 153, 40, 167, 111, 122, 218, 81, 232, 156, 160, 19, 187, 176, 242, 169, 227, 120, 168, 179, 96, 52, 152, 107, 145, 123, 184, 151, 7, 165, 182, 46, 112, 195, 160, 16, 150, 227, 92, 19, 219, 254 ], + "measurement": [ 10, 164, 78, 132, 174, 241, 130, 149, 222, 144, 49, 226, 67, 139, 196, 31, 52, 225, 206, 88, 230, 54, 166, 49, 204, 131, 171, 127, 193, 145, 239, 118, 249, 82, 128, 199, 169, 173, 147, 144, 232, 102, 235, 22, 91, 248, 30, 216 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 196, 49, 207, 89, 209, 12, 97, 47, 235, 160, 22, 231, 216, 231, 151, 66, 126, 104, 175, 55, 221, 249, 70, 139, 163, 14, 72, 38, 86, 19, 82, 237, 69, 139, 98, 86, 161, 184, 156, 67, 209, 75, 226, 25, 193, 19, 212, 151 ], + "measurement": [ 40, 118, 28, 168, 70, 224, 64, 214, 100, 4, 162, 87, 97, 171, 65, 173, 58, 154, 24, 105, 60, 133, 206, 37, 99, 236, 156, 223, 254, 55, 207, 172, 246, 54, 61, 136, 233, 83, 187, 136, 206, 251, 158, 43, 103, 127, 119, 114 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 150, 48, 243, 79, 173, 194, 224, 249, 157, 198, 63, 25, 225, 232, 113, 135, 248, 84, 200, 75, 155, 98, 77, 49, 174, 41, 23, 93, 226, 188, 169, 225, 31, 239, 24, 158, 160, 61, 20, 100, 74, 28, 173, 235, 81, 20, 239, 86 ], + "measurement": [ 126, 219, 193, 137, 242, 209, 198, 170, 251, 179, 170, 55, 84, 99, 45, 69, 28, 109, 239, 20, 132, 86, 175, 43, 161, 44, 37, 246, 50, 160, 212, 202, 242, 157, 250, 244, 146, 44, 16, 170, 110, 83, 69, 129, 139, 81, 116, 30 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 123, 158, 67, 13, 161, 18, 115, 246, 21, 58, 160, 227, 126, 51, 74, 97, 107, 171, 120, 20, 119, 86, 88, 212, 255, 23, 88, 206, 221, 60, 118, 230, 158, 79, 45, 225, 58, 32, 213, 161, 64, 138, 89, 180, 235, 204, 83, 191 ], + "measurement": [ 104, 145, 174, 38, 17, 214, 8, 132, 155, 229, 229, 13, 161, 54, 139, 62, 49, 239, 169, 126, 30, 240, 87, 122, 241, 178, 146, 87, 127, 46, 4, 79, 108, 160, 247, 99, 143, 217, 183, 17, 42, 114, 250, 71, 108, 55, 66, 142 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 228, 162, 55, 151, 65, 156, 196, 31, 125, 201, 135, 185, 242, 141, 210, 135, 70, 239, 131, 44, 190, 246, 150, 216, 169, 89, 15, 148, 86, 129, 24, 160, 6, 58, 241, 250, 106, 167, 82, 182, 244, 90, 194, 225, 1, 158, 167, 61 ], + "measurement": [ 48, 163, 104, 111, 165, 233, 157, 124, 140, 98, 49, 182, 156, 2, 244, 135, 52, 121, 42, 68, 43, 165, 80, 46, 239, 191, 62, 233, 131, 139, 141, 15, 216, 153, 165, 63, 67, 141, 166, 156, 162, 85, 71, 169, 235, 25, 242, 54 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 223, 53, 55, 76, 30, 172, 92, 59, 93, 170, 107, 2, 122, 130, 40, 57, 152, 136, 108, 84, 23, 212, 12, 180, 34, 1, 224, 137, 132, 166, 95, 110, 34, 11, 57, 89, 105, 147, 251, 7, 101, 121, 229, 41, 77, 179, 150, 123 ], + "measurement": [ 95, 93, 149, 160, 171, 205, 254, 68, 115, 167, 69, 26, 159, 71, 175, 4, 73, 36, 58, 29, 11, 215, 190, 201, 66, 197, 170, 174, 77, 177, 85, 235, 191, 71, 194, 151, 190, 211, 164, 49, 183, 138, 152, 239, 123, 87, 83, 240 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 1, 179, 164, 197, 194, 254, 225, 205, 162, 101, 171, 197, 104, 184, 89, 36, 73, 187, 150, 238, 26, 33, 83, 193, 49, 140, 14, 162, 154, 203, 159, 35, 17, 106, 131, 155, 252, 63, 191, 197, 121, 41, 106, 236, 51, 197, 27, 21 ], + "measurement": [ 110, 231, 68, 215, 217, 134, 191, 3, 2, 212, 186, 124, 24, 18, 38, 113, 170, 63, 164, 248, 181, 185, 255, 202, 109, 108, 164, 142, 68, 230, 157, 105, 89, 20, 177, 68, 5, 88, 241, 216, 254, 119, 12, 204, 57, 25, 73, 60 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 227, 23, 102, 71, 43, 109, 110, 141, 169, 212, 49, 239, 156, 94, 221, 220, 222, 160, 58, 84, 77, 53, 206, 4, 116, 28, 169, 38, 74, 183, 60, 193, 22, 214, 152, 194, 187, 95, 41, 229, 193, 116, 56, 199, 208, 10, 243, 66 ], + "measurement": [ 74, 29, 216, 211, 158, 76, 145, 152, 190, 95, 195, 122, 71, 95, 179, 198, 48, 107, 99, 254, 103, 45, 44, 202, 78, 207, 210, 161, 169, 219, 73, 189, 160, 51, 179, 102, 82, 34, 150, 148, 40, 217, 87, 150, 185, 237, 217, 222 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 236, 74, 73, 107, 245, 191, 172, 228, 145, 7, 154, 55, 137, 71, 101, 18, 230, 102, 196, 182, 17, 240, 193, 2, 252, 82, 156, 135, 47, 223, 92, 63, 58, 114, 62, 2, 176, 45, 43, 146, 182, 91, 18, 189, 157, 33, 234, 10 ], + "measurement": [ 62, 218, 131, 254, 38, 89, 53, 214, 14, 33, 42, 222, 124, 44, 190, 126, 26, 155, 214, 243, 77, 106, 226, 43, 64, 50, 203, 58, 201, 176, 253, 77, 100, 0, 47, 179, 104, 35, 226, 157, 93, 29, 150, 15, 7, 7, 58, 226 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 204, 87, 127, 213, 52, 161, 162, 47, 6, 114, 138, 148, 195, 85, 1, 58, 204, 153, 183, 41, 192, 72, 106, 61, 38, 241, 11, 144, 43, 69, 71, 170, 170, 12, 183, 12, 201, 226, 168, 195, 75, 221, 89, 214, 224, 42, 12, 15 ], + "measurement": [ 125, 85, 104, 138, 75, 24, 98, 129, 194, 247, 104, 201, 243, 7, 105, 179, 25, 144, 172, 83, 123, 101, 38, 245, 54, 40, 237, 22, 8, 181, 158, 125, 136, 100, 104, 157, 68, 121, 87, 244, 6, 31, 209, 32, 197, 76, 61, 3 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a65bab3c5878acb4525bb67f23fe5ae56dd2b0ee25fee4610a3bc9d03f992080", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", "vcpu_type": "EPYC-Turin" } } From e86ddd0d641d6a658998fd056bbf7b91dd2309c7 Mon Sep 17 00:00:00 2001 From: "pr-creation-bot-dfinity-ic[bot]" <200595415+pr-creation-bot-dfinity-ic[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:56:48 +0000 Subject: [PATCH 45/75] chore: Update Mainnet IC revisions canisters file (#10478) Update mainnet system canisters revisions file to include the latest WASM version released on the mainnet. This PR is created automatically using [`mainnet_revisions.py`](https://github.com/dfinity/ic/blob/master/ci/src/mainnet_revisions/mainnet_revisions.py) Co-authored-by: CI Automation --- mainnet-canister-revisions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mainnet-canister-revisions.json b/mainnet-canister-revisions.json index 79fbbe088693..174b1b6319e4 100644 --- a/mainnet-canister-revisions.json +++ b/mainnet-canister-revisions.json @@ -84,8 +84,8 @@ "sha256": "b443df3315902404b142d60f3cfd2f580181683310f6e6321b52de297deffcda" }, "internet_identity_backend": { - "sha256": "917a42d0f8621ab59921b3ad320bdc2fd30951721e1687e7e5d1be035936fce9", - "tag": "release-2026-06-12" + "sha256": "a2818676f07639a63b6b4ad55df0ad18868711ea305a1afb4d1167a080e2a911", + "tag": "release-2026-06-15" }, "internet_identity_frontend": { "sha256": "7f9e8557d25544dc00e72afc0f0c677e9823d66d07f8e65b671c102a44170c35", From 4be7ff817d4560583a31502c2a0450d923a60983 Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Tue, 16 Jun 2026 12:38:49 +0200 Subject: [PATCH 46/75] chore: remove mold (#10481) This replaces mold with lld. The mold linker was introduced because GNU's bfd was too slow and used too much memory; the gains compared to LLVM's lld on the other hand are marginal and we already have LLVM installed. --------- Co-authored-by: IDX GitHub Automation <> --- .devcontainer/devcontainer.json | 2 +- .github/workflows/api-bn-recovery-test.yml | 2 +- .github/workflows/ci-main.yml | 2 +- .github/workflows/ci-pr-only.yml | 2 +- .github/workflows/container-api-bn-recovery.yml | 2 +- .github/workflows/container-scan-nightly.yml | 2 +- .github/workflows/pocket-ic-tests-windows.yml | 2 +- .github/workflows/rate-limits-backend-release.yml | 2 +- .github/workflows/release-testing.yml | 2 +- .github/workflows/rosetta-release.yml | 2 +- .github/workflows/salt-sharing-canister-release.yml | 2 +- .github/workflows/schedule-daily.yml | 2 +- .github/workflows/schedule-rust-bench.yml | 2 +- .github/workflows/system-tests-benchmarks-nightly.yml | 2 +- .../workflows/update-mainnet-canister-revisions.yaml | 2 +- ci/container/Dockerfile | 11 +++++------ ci/container/TAG | 2 +- 17 files changed, 21 insertions(+), 22 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 61a711cea9c6..9aa957c2d21b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "image": "ghcr.io/dfinity/ic-dev@sha256:4413ff75554ac7e854fb54715e64641da25a551280d34b4a99c3acd6b5cef6d9", + "image": "ghcr.io/dfinity/ic-dev@sha256:e31592e1e2ea61f025bfb9960614333951c5ea32152666cdb00192b11f9955cf", "remoteUser": "ubuntu", "privileged": true, "runArgs": [ diff --git a/.github/workflows/api-bn-recovery-test.yml b/.github/workflows/api-bn-recovery-test.yml index c140867dd539..b2a0006349ce 100644 --- a/.github/workflows/api-bn-recovery-test.yml +++ b/.github/workflows/api-bn-recovery-test.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index c21afade622b..3fce8040cdeb 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -33,7 +33,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/ci-pr-only.yml b/.github/workflows/ci-pr-only.yml index 6b4e4e2cf4c5..bfc2f21295f7 100644 --- a/.github/workflows/ci-pr-only.yml +++ b/.github/workflows/ci-pr-only.yml @@ -37,7 +37,7 @@ jobs: runs-on: &dind-small-setup labels: dind-small container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --mount type=tmpfs,target="/tmp/containers" steps: diff --git a/.github/workflows/container-api-bn-recovery.yml b/.github/workflows/container-api-bn-recovery.yml index b339b301c172..d6dc87d25d0d 100644 --- a/.github/workflows/container-api-bn-recovery.yml +++ b/.github/workflows/container-api-bn-recovery.yml @@ -28,7 +28,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/container-scan-nightly.yml b/.github/workflows/container-scan-nightly.yml index 026876974257..7d53677a0e90 100644 --- a/.github/workflows/container-scan-nightly.yml +++ b/.github/workflows/container-scan-nightly.yml @@ -12,7 +12,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 60 diff --git a/.github/workflows/pocket-ic-tests-windows.yml b/.github/workflows/pocket-ic-tests-windows.yml index 93d159127c8b..dea14e9f4301 100644 --- a/.github/workflows/pocket-ic-tests-windows.yml +++ b/.github/workflows/pocket-ic-tests-windows.yml @@ -45,7 +45,7 @@ jobs: bazel-build-pocket-ic: name: Bazel Build PocketIC container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/rate-limits-backend-release.yml b/.github/workflows/rate-limits-backend-release.yml index c4a74330431e..a4cb6983cb61 100644 --- a/.github/workflows/rate-limits-backend-release.yml +++ b/.github/workflows/rate-limits-backend-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/release-testing.yml b/.github/workflows/release-testing.yml index 45b9d74a86ac..5d98d63bb579 100644 --- a/.github/workflows/release-testing.yml +++ b/.github/workflows/release-testing.yml @@ -35,7 +35,7 @@ jobs: group: dm1 labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 180 diff --git a/.github/workflows/rosetta-release.yml b/.github/workflows/rosetta-release.yml index be56b4100079..491a4f801537 100644 --- a/.github/workflows/rosetta-release.yml +++ b/.github/workflows/rosetta-release.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" environment: DockerHub diff --git a/.github/workflows/salt-sharing-canister-release.yml b/.github/workflows/salt-sharing-canister-release.yml index ffe5e25cf214..44c939b00eb2 100644 --- a/.github/workflows/salt-sharing-canister-release.yml +++ b/.github/workflows/salt-sharing-canister-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/schedule-daily.yml b/.github/workflows/schedule-daily.yml index 416600982377..39261854b99d 100644 --- a/.github/workflows/schedule-daily.yml +++ b/.github/workflows/schedule-daily.yml @@ -14,7 +14,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/schedule-rust-bench.yml b/.github/workflows/schedule-rust-bench.yml index 65c18cdd917f..bbe0ccc2ed93 100644 --- a/.github/workflows/schedule-rust-bench.yml +++ b/.github/workflows/schedule-rust-bench.yml @@ -24,7 +24,7 @@ jobs: # see linux-x86-64 runner group labels: rust-benchmarks container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 # running on bare metal machine using ubuntu user options: --user ubuntu --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/system-tests-benchmarks-nightly.yml b/.github/workflows/system-tests-benchmarks-nightly.yml index 692e1b0af597..8c534f9ecdeb 100644 --- a/.github/workflows/system-tests-benchmarks-nightly.yml +++ b/.github/workflows/system-tests-benchmarks-nightly.yml @@ -17,7 +17,7 @@ jobs: group: dm1 labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 480 diff --git a/.github/workflows/update-mainnet-canister-revisions.yaml b/.github/workflows/update-mainnet-canister-revisions.yaml index 9b06b3c9ad05..0fa35b675542 100644 --- a/.github/workflows/update-mainnet-canister-revisions.yaml +++ b/.github/workflows/update-mainnet-canister-revisions.yaml @@ -25,7 +25,7 @@ jobs: labels: dind-small environment: CREATE_PR container: - image: ghcr.io/dfinity/ic-build@sha256:1c970082c03d3be21b6e6749dda175a65e4a26535962ce7795868c20f0edc141 + image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" env: diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile index 7a0ffb3d9915..34976f850c8f 100644 --- a/ci/container/Dockerfile +++ b/ci/container/Dockerfile @@ -55,12 +55,11 @@ RUN curl -fsSL https://github.com/bazelbuild/bazelisk/releases/download/v1.28.1/ echo "$bazelisk_sha /usr/bin/bazel" | sha256sum --check && \ chmod 777 /usr/bin/bazel -# Add mold linker - required because it is 6x faster than ld -ARG MOLD_BIN="/usr/local/bin/mold" -ARG MOLD_VERSION=2.37.1 -RUN curl -sSL "https://github.com/rui314/mold/releases/download/v${MOLD_VERSION}/mold-${MOLD_VERSION}-$(uname -m)-linux.tar.gz" | tar -C /usr/local --strip-components=1 -xzf - && \ - echo "98a45fcc651424551f56b8bc4f697ae7ae66930da59b1ef9dfeeaa4da64cb2c6 ${MOLD_BIN}" | shasum -a 256 -c - && \ - ln -sf "${MOLD_BIN}" "$(realpath /usr/bin/ld)" +# Use lld as the default linker instead of GNU bfd ld (which is ~6x slower). +# For our large Rust debug binaries lld links roughly as fast as eg mold and +# with comparable memory use. Bazel uses its own hermetic (zig/lld) toolchain and +# is unaffected; this only changes the system linker used by cargo builds. +RUN ln -sf "$(command -v ld.lld)" "$(realpath /usr/bin/ld)" # Add motoko compiler ARG motoko_version=0.16.3 diff --git a/ci/container/TAG b/ci/container/TAG index dcc48239ab5b..8c244933577e 100644 --- a/ci/container/TAG +++ b/ci/container/TAG @@ -1 +1 @@ -aaa0c38e16eef4e2dcca91e82d210a125cbeea4dda546894730b164daaf3b06e +e1a2ccc7cc443f4b0d76cb9bca1fb2e8d4c5ce1a69d5ddd8be3b1b50ebd99e80 From adbc0c9e03e0d3fe004e7a25192e26c951162b71 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Tue, 16 Jun 2026 12:48:04 +0200 Subject: [PATCH 47/75] chore: upgrade UVM and P8s images (#10476) * [nixpkgs: 25.11 -> 26.05](https://github.com/dfinity-lab/farm/commit/adb884d87f3d5d0b41771de4557f9989225c97ef). * Fixes for the `nixos` hostname LLMNR conflicts: https://github.com/dfinity-lab/farm/commit/f34ec25475c1017aeb7dbff50d4781299f9d1ae7, https://github.com/dfinity-lab/farm/commit/9c749160a33eaf7fb322270b39aad7badd9519bb and https://github.com/dfinity-lab/farm/commit/e5b2bbf0aaf7c574bbb286fdcc55305ad99a7c8c. Tested that Prometheus and Grafana are working: Screenshot 2026-06-16 at 12 38 34 --- rs/tests/driver/src/driver/prometheus_vm.rs | 2 +- rs/tests/driver/src/driver/resource.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/tests/driver/src/driver/prometheus_vm.rs b/rs/tests/driver/src/driver/prometheus_vm.rs index fc09522908cb..8f7c2ff4ed4a 100644 --- a/rs/tests/driver/src/driver/prometheus_vm.rs +++ b/rs/tests/driver/src/driver/prometheus_vm.rs @@ -42,7 +42,7 @@ const PROMETHEUS_VM_NAME: &str = "prometheus"; /// Following through to the "Upload UVM images to S3" job and copying the from the line: /// upload: ../../../../../nix/store/...-nixos-disk-image-out-refs-discarded/nixos.img.zst to s3://dfinity-download/farm/prometheus-vm//x86_64-linux/prometheus-vm.img.zst const DEFAULT_PROMETHEUS_VM_IMG_SHA256: &str = - "49ecc84e46e5cc5b970fc9cf94aa3decd03161642c91f4d1153955110e0ee13f"; + "3568d6e7e8176636bcbd29f5469dec6b036345e61d2b57c276a0941df0b31c4a"; fn get_default_prometheus_vm_img_url() -> String { format!( diff --git a/rs/tests/driver/src/driver/resource.rs b/rs/tests/driver/src/driver/resource.rs index dc082789b0e6..615883619b3d 100644 --- a/rs/tests/driver/src/driver/resource.rs +++ b/rs/tests/driver/src/driver/resource.rs @@ -244,7 +244,7 @@ pub fn get_resource_request_for_nested_nodes( /// Following through to the "Upload UVM images to S3" job and copying the from the line: /// upload: ../../../../../nix/store/...-nixos-disk-image-out-refs-discarded/nixos.img.zst to s3://dfinity-download/farm/universal-vm//x86_64-linux/universal-vm.img.zst const DEFAULT_UNIVERSAL_VM_IMG_SHA256: &str = - "585f4cb5866a576a1ba85ffb2c4fbd1836c7e39a7e9a02b5be44bfb4820a1443"; + "ae94e672589c8cb47231976f8d0a4abaac4b8fde9ded1a664de6d7c32f0eac25"; pub fn get_resource_request_for_universal_vm( universal_vm: &UniversalVm, From e365598457915dd3102ac78ad5b5da2df57a4c58 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:15:03 +0200 Subject: [PATCH 48/75] chore: harden assert_no_snapshot and get_registry_version in migration canister (#10465) This PR hardens the following checks to work after subnet deletion: - asserting no snapshots succeeds if the call returns `DestinationInvalid` (if the canister or subnet was deleted, then the canister has no snapshots trivially); - waiting until a subnet is at a certain registry version only makes sense as long as the subnet exists. --- .../src/external_interfaces/management.rs | 35 +++++++++++++------ rs/migration_canister/src/processing.rs | 4 +-- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/rs/migration_canister/src/external_interfaces/management.rs b/rs/migration_canister/src/external_interfaces/management.rs index 3b2cf689c2df..4ab2f9e449ef 100644 --- a/rs/migration_canister/src/external_interfaces/management.rs +++ b/rs/migration_canister/src/external_interfaces/management.rs @@ -264,14 +264,18 @@ pub async fn rename_canister( pub async fn assert_no_snapshots(canister_id: Principal) -> ProcessingResult<(), ValidationError> { match list_canister_snapshots(&ListCanisterSnapshotsArgs { canister_id }).await { - Ok(snapshots) => { - if snapshots.is_empty() { - ProcessingResult::Success(()) - } else { - ProcessingResult::FatalFailure(ValidationError::ReplacedCanisterHasSnapshots( - Reserved, - )) - } + Ok(snapshots) if snapshots.is_empty() => ProcessingResult::Success(()), + Ok(_) => { + ProcessingResult::FatalFailure(ValidationError::ReplacedCanisterHasSnapshots(Reserved)) + } + Err(CallError::CallRejected(e)) + if e.reject_code() == Ok(RejectCode::DestinationInvalid) => + { + println!( + "Call `list_canister_snapshots` for {} returned DestinationInvalid, treating as success", + canister_id + ); + ProcessingResult::Success(()) } Err(e) => { println!( @@ -298,7 +302,9 @@ pub struct SubnetInfoResponse { pub registry_version: u64, } -pub async fn get_registry_version(subnet_id: Principal) -> ProcessingResult { +pub async fn get_registry_version( + subnet_id: Principal, +) -> ProcessingResult, Infallible> { let args = SubnetInfoArgs { subnet_id }; match Call::bounded_wait(subnet_id, "subnet_info") .with_arg(&args) @@ -307,7 +313,7 @@ pub async fn get_registry_version(subnet_id: Principal) -> ProcessingResult match response.candid::() { Ok(SubnetInfoResponse { registry_version, .. - }) => ProcessingResult::Success(registry_version), + }) => ProcessingResult::Success(Some(registry_version)), Err(e) => { println!( "Decoding `SubnetInfoResponse` for subnet: {} failed: {:?}", @@ -316,6 +322,15 @@ pub async fn get_registry_version(subnet_id: Principal) -> ProcessingResult + { + println!( + "Call `subnet_info` for subnet: {} returned DestinationInvalid, treating as success", + subnet_id + ); + ProcessingResult::Success(None) + } Err(e) => { println!( "Call `subnet_info` for subnet: {} failed: {:?}", diff --git a/rs/migration_canister/src/processing.rs b/rs/migration_canister/src/processing.rs index 5600af6ac141..826a88e60b22 100644 --- a/rs/migration_canister/src/processing.rs +++ b/rs/migration_canister/src/processing.rs @@ -273,8 +273,8 @@ pub async fn process_updated( else { return ProcessingResult::NoProgress; }; - if migrated_canister_subnet_version < registry_version - || replaced_canister_subnet_version < registry_version + if migrated_canister_subnet_version.is_some_and(|v| v < registry_version) + || replaced_canister_subnet_version.is_some_and(|v| v < registry_version) { return ProcessingResult::NoProgress; } From e6a0f286a8181b3e1800aae7f25d32d24e93a2cc Mon Sep 17 00:00:00 2001 From: "pr-creation-bot-dfinity-ic[bot]" <200595415+pr-creation-bot-dfinity-ic[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:25:28 +0000 Subject: [PATCH 49/75] chore: Update Mainnet ICOS revisions file (#10483) Update mainnet revisions file to include the latest version released on the mainnet. This PR is created automatically using [`mainnet_revisions.py`](https://github.com/dfinity/ic/blob/master/ci/src/mainnet_revisions/mainnet_revisions.py) Co-authored-by: CI Automation --- mainnet-icos-revisions.json | 78 ++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/mainnet-icos-revisions.json b/mainnet-icos-revisions.json index eb60da5e1d5f..ff287291ace2 100644 --- a/mainnet-icos-revisions.json +++ b/mainnet-icos-revisions.json @@ -135,45 +135,45 @@ } }, "io67a-2jmkw-zup3h-snbwi-g6a5n-rm5dn-b6png-lvdpl-nqnto-yih6l-gqe": { - "version": "fb721da900b9e9219773ee312f987971338f7c62", - "update_img_hash": "3e6cb724f0cc0a17d1692e91e1f95cfc883c4f6e49146cd57921e69617cedabc", - "update_img_hash_dev": "2ee8c2809aadef08645c453c0388afbb53db410e47665d66a7f214af3334d4e6", + "version": "93274f1726d9575389004069c4a46a32084051f5", + "update_img_hash": "04d2942487e54378ca67e108dd105cd1f5e94df62edd5647315433d9e19c1876", + "update_img_hash_dev": "a8869b3eff8dd068f1fbe85d54d06d669649a4cda395a5e0163126105267367c", "launch_measurements": { "guest_launch_measurements": [ { - "measurement": [ 18, 227, 13, 20, 22, 234, 253, 96, 194, 4, 236, 231, 88, 29, 125, 41, 18, 235, 183, 80, 143, 222, 243, 72, 27, 183, 170, 66, 233, 17, 228, 16, 243, 245, 27, 238, 113, 52, 139, 136, 225, 121, 209, 248, 53, 67, 198, 219 ], + "measurement": [ 44, 60, 43, 207, 106, 72, 49, 121, 211, 88, 60, 211, 4, 216, 201, 14, 185, 135, 39, 138, 108, 41, 100, 146, 208, 182, 92, 156, 150, 25, 7, 155, 44, 6, 73, 200, 58, 66, 179, 174, 49, 26, 17, 169, 79, 76, 157, 180 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=284aa1fe6b122844005a115105e333e5ea85d30df4230d5c2648dd8286184edc" } }, { - "measurement": [ 168, 20, 35, 37, 80, 160, 194, 73, 121, 117, 175, 87, 158, 43, 227, 86, 23, 93, 118, 104, 10, 43, 72, 8, 251, 95, 53, 210, 107, 57, 159, 101, 251, 163, 160, 61, 240, 8, 82, 78, 93, 109, 19, 238, 10, 147, 125, 128 ], + "measurement": [ 206, 29, 43, 50, 145, 91, 131, 177, 139, 234, 96, 57, 69, 127, 166, 79, 45, 237, 225, 76, 51, 241, 114, 12, 136, 0, 238, 84, 29, 255, 226, 154, 4, 21, 48, 138, 188, 123, 0, 112, 116, 161, 164, 96, 138, 105, 6, 14 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=284aa1fe6b122844005a115105e333e5ea85d30df4230d5c2648dd8286184edc" } }, { - "measurement": [ 246, 185, 48, 139, 248, 209, 51, 77, 211, 115, 215, 117, 123, 115, 148, 85, 62, 80, 126, 125, 231, 190, 189, 43, 202, 76, 117, 228, 17, 216, 164, 127, 98, 197, 166, 229, 95, 100, 23, 3, 155, 136, 114, 79, 147, 237, 61, 140 ], + "measurement": [ 20, 156, 136, 60, 131, 119, 111, 195, 76, 147, 206, 6, 141, 21, 210, 88, 193, 30, 78, 160, 30, 220, 51, 165, 142, 108, 46, 55, 188, 85, 214, 12, 38, 46, 116, 86, 86, 118, 187, 190, 152, 221, 152, 210, 137, 14, 45, 89 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=284aa1fe6b122844005a115105e333e5ea85d30df4230d5c2648dd8286184edc" } }, { - "measurement": [ 140, 175, 182, 40, 232, 142, 53, 109, 105, 198, 36, 254, 12, 229, 156, 206, 37, 54, 180, 212, 177, 103, 188, 232, 5, 192, 187, 63, 54, 71, 185, 162, 40, 157, 245, 126, 92, 228, 179, 98, 80, 209, 212, 163, 4, 68, 170, 72 ], + "measurement": [ 226, 95, 109, 98, 188, 225, 127, 178, 167, 215, 19, 120, 78, 71, 185, 159, 55, 65, 16, 100, 148, 214, 104, 77, 253, 35, 58, 11, 96, 146, 164, 30, 190, 45, 128, 217, 131, 97, 141, 64, 70, 12, 102, 149, 37, 124, 246, 139 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=284aa1fe6b122844005a115105e333e5ea85d30df4230d5c2648dd8286184edc" } }, { - "measurement": [ 127, 58, 219, 103, 155, 159, 173, 5, 157, 169, 196, 5, 3, 123, 239, 211, 19, 224, 219, 31, 211, 124, 22, 206, 210, 232, 7, 66, 155, 60, 10, 47, 175, 183, 125, 165, 202, 58, 51, 113, 147, 128, 84, 165, 83, 152, 197, 26 ], + "measurement": [ 142, 207, 7, 217, 143, 100, 229, 58, 113, 3, 26, 29, 253, 255, 232, 205, 60, 160, 86, 175, 166, 183, 120, 203, 237, 94, 28, 242, 197, 19, 87, 198, 147, 70, 217, 19, 123, 97, 195, 65, 98, 37, 57, 225, 219, 232, 79, 140 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=284aa1fe6b122844005a115105e333e5ea85d30df4230d5c2648dd8286184edc" } }, { - "measurement": [ 210, 100, 7, 43, 164, 116, 27, 36, 179, 36, 71, 190, 75, 116, 20, 196, 19, 143, 23, 45, 144, 105, 69, 99, 154, 222, 226, 77, 29, 137, 40, 136, 56, 164, 49, 179, 4, 205, 95, 126, 79, 94, 206, 246, 75, 215, 175, 103 ], + "measurement": [ 30, 207, 134, 146, 153, 30, 122, 75, 210, 41, 65, 155, 228, 249, 108, 64, 127, 221, 97, 68, 45, 17, 112, 31, 211, 117, 120, 179, 104, 193, 166, 74, 172, 128, 155, 178, 161, 90, 127, 97, 115, 127, 241, 165, 186, 65, 229, 86 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=fa2e2786b6236d7afa6242cf7f17b8d96699b3e99bb0d46e83e9265648a7b3a5" + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=284aa1fe6b122844005a115105e333e5ea85d30df4230d5c2648dd8286184edc" } } ] @@ -181,86 +181,86 @@ "launch_measurements_dev": { "guest_launch_measurements": [ { - "measurement": [ 85, 172, 241, 226, 57, 162, 121, 60, 236, 234, 186, 245, 106, 55, 155, 5, 18, 59, 67, 128, 45, 108, 59, 235, 157, 88, 65, 159, 197, 246, 199, 87, 197, 177, 172, 199, 130, 130, 97, 213, 183, 14, 78, 255, 28, 93, 37, 6 ], + "measurement": [ 105, 173, 148, 238, 8, 250, 185, 63, 150, 87, 88, 156, 150, 152, 55, 113, 141, 1, 112, 7, 19, 158, 96, 209, 90, 89, 64, 87, 13, 46, 52, 107, 66, 23, 95, 228, 197, 231, 172, 142, 79, 22, 108, 253, 123, 24, 92, 209 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 167, 190, 44, 164, 33, 11, 140, 155, 83, 189, 161, 253, 157, 69, 71, 100, 245, 16, 21, 9, 116, 142, 112, 80, 205, 152, 90, 248, 194, 135, 190, 123, 191, 135, 240, 208, 183, 188, 120, 168, 180, 210, 131, 47, 149, 71, 222, 90 ], + "measurement": [ 191, 64, 121, 166, 225, 17, 13, 190, 239, 142, 253, 172, 106, 9, 159, 110, 218, 206, 33, 127, 145, 232, 204, 141, 14, 141, 116, 142, 89, 201, 120, 38, 20, 242, 237, 248, 19, 243, 0, 253, 94, 128, 132, 72, 110, 239, 60, 225 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 10, 164, 78, 132, 174, 241, 130, 149, 222, 144, 49, 226, 67, 139, 196, 31, 52, 225, 206, 88, 230, 54, 166, 49, 204, 131, 171, 127, 193, 145, 239, 118, 249, 82, 128, 199, 169, 173, 147, 144, 232, 102, 235, 22, 91, 248, 30, 216 ], + "measurement": [ 63, 135, 52, 135, 252, 133, 246, 94, 53, 224, 248, 20, 202, 117, 97, 51, 75, 176, 115, 34, 37, 25, 74, 191, 202, 4, 137, 174, 97, 130, 42, 222, 49, 211, 41, 245, 63, 64, 94, 169, 159, 239, 58, 179, 205, 248, 159, 193 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 40, 118, 28, 168, 70, 224, 64, 214, 100, 4, 162, 87, 97, 171, 65, 173, 58, 154, 24, 105, 60, 133, 206, 37, 99, 236, 156, 223, 254, 55, 207, 172, 246, 54, 61, 136, 233, 83, 187, 136, 206, 251, 158, 43, 103, 127, 119, 114 ], + "measurement": [ 70, 46, 124, 96, 33, 117, 56, 76, 62, 198, 253, 108, 61, 202, 147, 196, 244, 157, 62, 195, 99, 212, 0, 237, 1, 7, 207, 54, 91, 194, 237, 171, 48, 101, 178, 103, 142, 239, 202, 114, 186, 158, 84, 215, 126, 111, 55, 7 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-v4" } }, { - "measurement": [ 126, 219, 193, 137, 242, 209, 198, 170, 251, 179, 170, 55, 84, 99, 45, 69, 28, 109, 239, 20, 132, 86, 175, 43, 161, 44, 37, 246, 50, 160, 212, 202, 242, 157, 250, 244, 146, 44, 16, 170, 110, 83, 69, 129, 139, 81, 116, 30 ], + "measurement": [ 165, 224, 48, 69, 183, 221, 13, 75, 131, 31, 73, 76, 102, 114, 25, 88, 199, 233, 91, 143, 135, 75, 199, 90, 187, 211, 228, 39, 64, 185, 216, 195, 73, 136, 122, 97, 175, 21, 82, 82, 57, 70, 70, 81, 30, 99, 173, 121 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 104, 145, 174, 38, 17, 214, 8, 132, 155, 229, 229, 13, 161, 54, 139, 62, 49, 239, 169, 126, 30, 240, 87, 122, 241, 178, 146, 87, 127, 46, 4, 79, 108, 160, 247, 99, 143, 217, 183, 17, 42, 114, 250, 71, 108, 55, 66, 142 ], + "measurement": [ 17, 109, 250, 51, 85, 244, 175, 124, 80, 228, 110, 82, 8, 4, 19, 200, 49, 156, 146, 6, 76, 126, 72, 88, 253, 175, 135, 186, 159, 121, 141, 191, 60, 212, 213, 183, 21, 3, 50, 145, 32, 144, 39, 21, 105, 85, 168, 59 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 48, 163, 104, 111, 165, 233, 157, 124, 140, 98, 49, 182, 156, 2, 244, 135, 52, 121, 42, 68, 43, 165, 80, 46, 239, 191, 62, 233, 131, 139, 141, 15, 216, 153, 165, 63, 67, 141, 166, 156, 162, 85, 71, 169, 235, 25, 242, 54 ], + "measurement": [ 133, 194, 89, 247, 162, 37, 46, 54, 185, 43, 246, 118, 64, 242, 7, 49, 75, 200, 76, 24, 145, 38, 195, 79, 210, 222, 222, 73, 183, 163, 86, 227, 112, 214, 205, 130, 195, 147, 78, 121, 215, 226, 162, 61, 63, 216, 113, 127 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 95, 93, 149, 160, 171, 205, 254, 68, 115, 167, 69, 26, 159, 71, 175, 4, 73, 36, 58, 29, 11, 215, 190, 201, 66, 197, 170, 174, 77, 177, 85, 235, 191, 71, 194, 151, 190, 211, 164, 49, 183, 138, 152, 239, 123, 87, 83, 240 ], + "measurement": [ 241, 124, 161, 85, 92, 228, 187, 188, 25, 243, 171, 50, 182, 148, 172, 0, 74, 106, 116, 111, 3, 7, 38, 175, 71, 195, 64, 233, 78, 133, 90, 219, 251, 21, 249, 158, 151, 167, 158, 65, 209, 76, 183, 23, 92, 182, 156, 178 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-Genoa" } }, { - "measurement": [ 110, 231, 68, 215, 217, 134, 191, 3, 2, 212, 186, 124, 24, 18, 38, 113, 170, 63, 164, 248, 181, 185, 255, 202, 109, 108, 164, 142, 68, 230, 157, 105, 89, 20, 177, 68, 5, 88, 241, 216, 254, 119, 12, 204, 57, 25, 73, 60 ], + "measurement": [ 142, 189, 119, 160, 139, 253, 16, 53, 3, 19, 120, 213, 144, 242, 110, 16, 96, 174, 153, 24, 201, 209, 115, 144, 153, 105, 113, 57, 90, 110, 192, 39, 254, 127, 27, 47, 212, 189, 124, 198, 30, 65, 132, 92, 176, 165, 216, 181 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 74, 29, 216, 211, 158, 76, 145, 152, 190, 95, 195, 122, 71, 95, 179, 198, 48, 107, 99, 254, 103, 45, 44, 202, 78, 207, 210, 161, 169, 219, 73, 189, 160, 51, 179, 102, 82, 34, 150, 148, 40, 217, 87, 150, 185, 237, 217, 222 ], + "measurement": [ 75, 178, 192, 83, 32, 97, 32, 141, 136, 50, 226, 59, 102, 193, 194, 14, 114, 153, 36, 194, 228, 114, 122, 227, 135, 86, 6, 168, 227, 102, 210, 145, 168, 144, 154, 7, 250, 105, 46, 43, 86, 244, 22, 184, 8, 150, 151, 40 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 62, 218, 131, 254, 38, 89, 53, 214, 14, 33, 42, 222, 124, 44, 190, 126, 26, 155, 214, 243, 77, 106, 226, 43, 64, 50, 203, 58, 201, 176, 253, 77, 100, 0, 47, 179, 104, 35, 226, 157, 93, 29, 150, 15, 7, 7, 58, 226 ], + "measurement": [ 210, 247, 162, 47, 251, 45, 209, 18, 114, 5, 182, 203, 185, 124, 32, 192, 98, 111, 209, 246, 113, 105, 147, 103, 67, 100, 241, 65, 109, 107, 207, 161, 39, 241, 213, 20, 94, 241, 27, 125, 17, 109, 87, 226, 139, 134, 196, 177 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 console=tty0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/7c0a626e-e5ea-e543-b5c5-300eb8304db7 console=ttyS0 nomodeset dfinity.system=A dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-Turin" } }, { - "measurement": [ 125, 85, 104, 138, 75, 24, 98, 129, 194, 247, 104, 201, 243, 7, 105, 179, 25, 144, 172, 83, 123, 101, 38, 245, 54, 40, 237, 22, 8, 181, 158, 125, 136, 100, 104, 157, 68, 121, 87, 244, 6, 31, 209, 32, 197, 76, 61, 3 ], + "measurement": [ 226, 20, 167, 204, 44, 9, 27, 163, 63, 189, 129, 129, 143, 204, 198, 104, 221, 99, 13, 64, 4, 138, 136, 64, 230, 16, 131, 69, 167, 253, 52, 78, 24, 172, 35, 234, 56, 91, 99, 164, 38, 201, 78, 96, 91, 27, 15, 53 ], "metadata": { - "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 console=tty0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=a4aec342e00cb1c2934d5ba4792d429d536538fc6029f7c02d1f445bfac6d508", + "kernel_cmdline": "root=/dev/disk/by-partuuid/a78bc3a8-376c-054a-96e7-3904b915d0c5 console=ttyS0 nomodeset dfinity.system=B dfinity.tee=1 security=selinux selinux=1 enforcing=1 root_hash=bb8c16de47f4323c7f955e7374e3d49f4dfa75ea5e7bf9cd67a34ccbb7d0d429", "vcpu_type": "EPYC-Turin" } } From 7d9bbd3e83bc99a79603bc5d662cf4b95d4bab59 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn <99166915+eichhorl@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:09:55 +0200 Subject: [PATCH 50/75] feat: Enable `PATCH` for HTTP outcalls (#10453) https://github.com/dfinity/ic/pull/10378 introduced support for the `PATCH` method to the HTTP outcalls implementation. For backward compatibility reasons, the new method remained disabled such that requests specifying it are still rejected. The change above is being rolled out this week as part of release [142256](https://dashboard.internetcomputer.org/proposal/142256). Therefore, this PR enables the `PATCH` method such that it may be used in HTTP outcall requests. For backward compatibility reasons, note that subnets upgraded to the version of this PR should not directly be downgraded to versions earlier than release [142256](https://dashboard.internetcomputer.org/proposal/142256) mentioned above. This reverts commit 26e3ae7d1b4cb5682edcaa4d828baa27fc7943e6. --- .../canister_http_correctness_test.rs | 42 ++++++++-- rs/types/types/src/canister_http.rs | 78 ++++--------------- 2 files changed, 50 insertions(+), 70 deletions(-) diff --git a/rs/tests/networking/canister_http_correctness_test.rs b/rs/tests/networking/canister_http_correctness_test.rs index 154393941343..7eab79498d41 100644 --- a/rs/tests/networking/canister_http_correctness_test.rs +++ b/rs/tests/networking/canister_http_correctness_test.rs @@ -128,7 +128,8 @@ fn main() -> Result<()> { .add_test(systest!(test_put_without_non_replicated_rejected)) .add_test(systest!(test_delete_call)) .add_test(systest!(test_delete_without_non_replicated_rejected)) - .add_test(systest!(test_patch_rejected)) + .add_test(systest!(test_patch_call)) + .add_test(systest!(test_patch_without_non_replicated_rejected)) .add_test(systest!(test_max_possible_request_size)) .add_test(systest!(test_max_possible_request_size_exceeded)) // This section tests the request headers limits scenarios @@ -1941,11 +1942,7 @@ fn test_delete_without_non_replicated_rejected(env: TestEnv) { }); } -// PATCH is plumbed through the types but not yet enabled on replicated subnets: -// the execution layer rejects it so no PATCH context enters the replicated -// state until support has rolled out to all replicas. This holds even for a -// non-replicated request. -fn test_patch_rejected(env: TestEnv) { +fn test_patch_call(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); @@ -1975,8 +1972,39 @@ fn test_patch_rejected(env: TestEnv) { }, )); + assert_matches!(response, Ok(response) => { + assert_matches!(response, RemoteHttpResponse { status: 200, .. }); + assert_distinct_headers(&response); + assert_http_json_response(&request, &response); + }); +} + +fn test_patch_without_non_replicated_rejected(env: TestEnv) { + let handlers = Handlers::new(&env); + let webserver_ipv6 = get_universal_vm_address(&env); + + let url = format!("https://[{}]/{}", webserver_ipv6, "anything"); + let request = UnvalidatedCanisterHttpRequestArgs { + url, + headers: vec![], + method: HttpMethod::PATCH, + body: Some(vec![]), + transform: None, + max_response_bytes: Some(1024), + is_replicated: None, + pricing_version: None, + }; + + let (response, _) = block_on(submit_outcall( + &handlers, + RemoteHttpRequest { + request, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, + }, + )); + assert_matches!(response, Err(RejectResponse { reject_message, .. }) => { - assert!(reject_message.contains("PATCH HTTP method is not yet supported")); + assert!(reject_message.contains("only allowed for non-replicated requests")); }); } diff --git a/rs/types/types/src/canister_http.rs b/rs/types/types/src/canister_http.rs index fe452bc7b2d6..20ef87a92f22 100644 --- a/rs/types/types/src/canister_http.rs +++ b/rs/types/types/src/canister_http.rs @@ -534,26 +534,19 @@ impl CanisterHttpRequestContext { None => Ok(None), }?; - // PATCH is plumbed through the types but not yet enabled on replicated - // subnets: reject it here in the execution layer so that no PATCH - // `http_method` enters the replicated state until support has rolled - // out to all replicas (mirroring how PUT/DELETE were staged in #8715 / - // #8717). A follow-up PR enables it once the rollout is complete. - if matches!(args.method, HttpMethod::PATCH) { - return Err(CanisterHttpRequestContextError::HttpMethodNotYetSupported); - } - - // Allow PUT and DELETE only in non-replicated mode to avoid + // Allow PUT, DELETE, and PATCH only in non-replicated mode to avoid // confusing race conditions that may occur. // For example, if first a DELETE outcall for resource R is made, - // directly followed by a PUT or POST outcall for R, in + // directly followed by a PUT, PATCH, or POST outcall for R, in // replicated mode it may happen that R is actually deleted after the - // PUT/POST outcall has finished, because the IC does not + // PUT/PATCH/POST outcall has finished, because the IC does not // necessarily wait for all outcalls to complete before a result is // delivered back to the canister: The IC only waits for sufficient // calls to complete to reach consensus on the result. - if matches!(args.method, HttpMethod::PUT | HttpMethod::DELETE) - && args.is_replicated != Some(false) + if matches!( + args.method, + HttpMethod::PUT | HttpMethod::DELETE | HttpMethod::PATCH + ) && args.is_replicated != Some(false) { return Err(CanisterHttpRequestContextError::DeterministicResponseCountRequired); } @@ -665,12 +658,10 @@ impl CanisterHttpRequestContext { // does not necessarily wait for all outcalls to complete before a result // is delivered back to the canister: The IC only waits for sufficient // calls to complete to reach consensus on the result. - // PATCH is not yet enabled on replicated subnets (see generate_from_args). - if matches!(args.method, HttpMethod::PATCH) { - return Err(CanisterHttpRequestContextError::HttpMethodNotYetSupported); - } - if matches!(args.method, HttpMethod::PUT | HttpMethod::DELETE) - && !(min_responses == max_responses && max_responses == total_requests) + if matches!( + args.method, + HttpMethod::PUT | HttpMethod::DELETE | HttpMethod::PATCH + ) && !(min_responses == max_responses && max_responses == total_requests) { return Err(CanisterHttpRequestContextError::DeterministicResponseCountRequired); } @@ -740,10 +731,6 @@ pub enum CanisterHttpRequestContextError { NoNodesAvailableForDelegation, DeterministicResponseCountRequired, InvalidReplicationCounts(String), - /// The requested HTTP method is plumbed through the types but not yet - /// enabled on replicated subnets (currently PATCH); rejected until support - /// has rolled out to all replicas. - HttpMethodNotYetSupported, } impl From for UserError { @@ -811,10 +798,6 @@ impl From for UserError { CanisterHttpRequestContextError::InvalidReplicationCounts(msg) => { UserError::new(ErrorCode::CanisterRejectedMessage, msg) } - CanisterHttpRequestContextError::HttpMethodNotYetSupported => UserError::new( - ErrorCode::CanisterRejectedMessage, - "The PATCH HTTP method is not yet supported.".to_string(), - ), } } } @@ -1421,6 +1404,7 @@ mod tests { #[rstest] #[case(HttpMethod::PUT)] #[case(HttpMethod::DELETE)] + #[case(HttpMethod::PATCH)] fn put_delete_requires_non_replicated(#[case] method: HttpMethod) { let rng = &mut ReproducibleRng::new(); let node_ids = BTreeSet::from([node_test_id(1)]); @@ -1461,41 +1445,6 @@ mod tests { ); } - #[test] - fn patch_is_rejected_until_rollout() { - let rng = &mut ReproducibleRng::new(); - let node_ids = BTreeSet::from([node_test_id(1), node_test_id(2), node_test_id(3)]); - let request = dummy_request(); - - // Rejected outright, regardless of is_replicated, so no PATCH context - // enters the replicated state until support has rolled out. - for is_replicated in [None, Some(true), Some(false)] { - let args = dummy_args(HttpMethod::PATCH, is_replicated); - let result = CanisterHttpRequestContext::generate_from_args( - UNIX_EPOCH, &request, args, &node_ids, rng, - ); - assert_matches!( - result, - Err(CanisterHttpRequestContextError::HttpMethodNotYetSupported) - ); - } - - // Also rejected via the flexible API. - let mut args = dummy_flexible_args(Some(ReplicationCounts { - total_requests: 3, - min_responses: 3, - max_responses: 3, - })); - args.method = HttpMethod::PATCH; - let result = CanisterHttpRequestContext::generate_from_flexible_args( - UNIX_EPOCH, &request, args, &node_ids, rng, - ); - assert_matches!( - result, - Err(CanisterHttpRequestContextError::HttpMethodNotYetSupported) - ); - } - #[test] fn rejects_invalid_transform_principal() { let rng = &mut ReproducibleRng::new(); @@ -1893,10 +1842,13 @@ mod tests { #[rstest] #[case(HttpMethod::PUT, 3, 2, 3, false)] #[case(HttpMethod::DELETE, 3, 2, 3, false)] + #[case(HttpMethod::PATCH, 3, 2, 3, false)] #[case(HttpMethod::PUT, 3, 3, 3, true)] #[case(HttpMethod::DELETE, 3, 3, 3, true)] + #[case(HttpMethod::PATCH, 3, 3, 3, true)] #[case(HttpMethod::PUT, 3, 2, 2, false)] #[case(HttpMethod::DELETE, 3, 2, 2, false)] + #[case(HttpMethod::PATCH, 3, 2, 2, false)] fn flexible_methods_require_deterministic_response_counts( #[case] method: HttpMethod, #[case] total_requests: u32, From 9ae9d253f7302cbc6bb4f7b84ec3f4460400ede4 Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Tue, 16 Jun 2026 17:13:39 +0200 Subject: [PATCH 51/75] fix: use curl for fetching gh signature (#10488) This updates the Dockerfile to use `curl` instead of `wget` when fetching the GitHub signature. The `wget` package is not installed explicitly until later via `package.dev`; it just happens to be there because it is an implicit dependency of the android platform-tools deb package we install in `ic-build`. --------- Co-authored-by: IDX GitHub Automation <> --- .devcontainer/devcontainer.json | 2 +- .github/workflows/api-bn-recovery-test.yml | 2 +- .github/workflows/ci-main.yml | 2 +- .github/workflows/ci-pr-only.yml | 2 +- .github/workflows/container-api-bn-recovery.yml | 2 +- .github/workflows/container-scan-nightly.yml | 2 +- .github/workflows/pocket-ic-tests-windows.yml | 2 +- .github/workflows/rate-limits-backend-release.yml | 2 +- .github/workflows/release-testing.yml | 2 +- .github/workflows/rosetta-release.yml | 2 +- .github/workflows/salt-sharing-canister-release.yml | 2 +- .github/workflows/schedule-daily.yml | 2 +- .github/workflows/schedule-rust-bench.yml | 2 +- .github/workflows/system-tests-benchmarks-nightly.yml | 2 +- .github/workflows/update-mainnet-canister-revisions.yaml | 2 +- ci/container/Dockerfile | 2 +- ci/container/TAG | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9aa957c2d21b..fdb6c4d212c1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "image": "ghcr.io/dfinity/ic-dev@sha256:e31592e1e2ea61f025bfb9960614333951c5ea32152666cdb00192b11f9955cf", + "image": "ghcr.io/dfinity/ic-dev@sha256:34f2713d09170e6def94ea81f1175af1505bb408162bcf3a613ac43b58a699e1", "remoteUser": "ubuntu", "privileged": true, "runArgs": [ diff --git a/.github/workflows/api-bn-recovery-test.yml b/.github/workflows/api-bn-recovery-test.yml index b2a0006349ce..fc50053f67fe 100644 --- a/.github/workflows/api-bn-recovery-test.yml +++ b/.github/workflows/api-bn-recovery-test.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 3fce8040cdeb..9306d2946076 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -33,7 +33,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/ci-pr-only.yml b/.github/workflows/ci-pr-only.yml index bfc2f21295f7..cfee71a972ec 100644 --- a/.github/workflows/ci-pr-only.yml +++ b/.github/workflows/ci-pr-only.yml @@ -37,7 +37,7 @@ jobs: runs-on: &dind-small-setup labels: dind-small container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --mount type=tmpfs,target="/tmp/containers" steps: diff --git a/.github/workflows/container-api-bn-recovery.yml b/.github/workflows/container-api-bn-recovery.yml index d6dc87d25d0d..a0984dd11d34 100644 --- a/.github/workflows/container-api-bn-recovery.yml +++ b/.github/workflows/container-api-bn-recovery.yml @@ -28,7 +28,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/container-scan-nightly.yml b/.github/workflows/container-scan-nightly.yml index 7d53677a0e90..404921677da0 100644 --- a/.github/workflows/container-scan-nightly.yml +++ b/.github/workflows/container-scan-nightly.yml @@ -12,7 +12,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 60 diff --git a/.github/workflows/pocket-ic-tests-windows.yml b/.github/workflows/pocket-ic-tests-windows.yml index dea14e9f4301..e73cfbd30c3f 100644 --- a/.github/workflows/pocket-ic-tests-windows.yml +++ b/.github/workflows/pocket-ic-tests-windows.yml @@ -45,7 +45,7 @@ jobs: bazel-build-pocket-ic: name: Bazel Build PocketIC container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/rate-limits-backend-release.yml b/.github/workflows/rate-limits-backend-release.yml index a4cb6983cb61..2f1f05b1beac 100644 --- a/.github/workflows/rate-limits-backend-release.yml +++ b/.github/workflows/rate-limits-backend-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/release-testing.yml b/.github/workflows/release-testing.yml index 5d98d63bb579..10181548e6a7 100644 --- a/.github/workflows/release-testing.yml +++ b/.github/workflows/release-testing.yml @@ -35,7 +35,7 @@ jobs: group: dm1 labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 180 diff --git a/.github/workflows/rosetta-release.yml b/.github/workflows/rosetta-release.yml index 491a4f801537..2bf60c222423 100644 --- a/.github/workflows/rosetta-release.yml +++ b/.github/workflows/rosetta-release.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" environment: DockerHub diff --git a/.github/workflows/salt-sharing-canister-release.yml b/.github/workflows/salt-sharing-canister-release.yml index 44c939b00eb2..a83676ed5e79 100644 --- a/.github/workflows/salt-sharing-canister-release.yml +++ b/.github/workflows/salt-sharing-canister-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/schedule-daily.yml b/.github/workflows/schedule-daily.yml index 39261854b99d..a8bec0dcb481 100644 --- a/.github/workflows/schedule-daily.yml +++ b/.github/workflows/schedule-daily.yml @@ -14,7 +14,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/schedule-rust-bench.yml b/.github/workflows/schedule-rust-bench.yml index bbe0ccc2ed93..fedc50a34dc9 100644 --- a/.github/workflows/schedule-rust-bench.yml +++ b/.github/workflows/schedule-rust-bench.yml @@ -24,7 +24,7 @@ jobs: # see linux-x86-64 runner group labels: rust-benchmarks container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 # running on bare metal machine using ubuntu user options: --user ubuntu --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/system-tests-benchmarks-nightly.yml b/.github/workflows/system-tests-benchmarks-nightly.yml index 8c534f9ecdeb..6ace65447f6b 100644 --- a/.github/workflows/system-tests-benchmarks-nightly.yml +++ b/.github/workflows/system-tests-benchmarks-nightly.yml @@ -17,7 +17,7 @@ jobs: group: dm1 labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 480 diff --git a/.github/workflows/update-mainnet-canister-revisions.yaml b/.github/workflows/update-mainnet-canister-revisions.yaml index 0fa35b675542..4323c2980d69 100644 --- a/.github/workflows/update-mainnet-canister-revisions.yaml +++ b/.github/workflows/update-mainnet-canister-revisions.yaml @@ -25,7 +25,7 @@ jobs: labels: dind-small environment: CREATE_PR container: - image: ghcr.io/dfinity/ic-build@sha256:708a4b0d448319a04c117c4fb03bf90ca3e1df11bc360837574ccae013239893 + image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" env: diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile index 34976f850c8f..d2a52f7b5f40 100644 --- a/ci/container/Dockerfile +++ b/ci/container/Dockerfile @@ -96,7 +96,7 @@ FROM base as dev # Add GitHub CLI repository to get the latest version RUN mkdir -p -m 755 /etc/apt/keyrings \ - && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && out=$(mktemp) && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o "$out" \ && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ && mkdir -p -m 755 /etc/apt/sources.list.d \ diff --git a/ci/container/TAG b/ci/container/TAG index 8c244933577e..43f8dcd6bc94 100644 --- a/ci/container/TAG +++ b/ci/container/TAG @@ -1 +1 @@ -e1a2ccc7cc443f4b0d76cb9bca1fb2e8d4c5ce1a69d5ddd8be3b1b50ebd99e80 +2d6069cd38193736e69b7edda7d96202452405a85670f82b3dc8674ac2ef9ebd From c808ef10800aab828a76ce027de7495dee77829a Mon Sep 17 00:00:00 2001 From: Stefan Schneider <31004026+schneiderstefan@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:16:30 +0200 Subject: [PATCH 52/75] fix: Revert "chore(Core): Limit wasm locals" (#10475) Reverts dfinity/ic#10430 --- rs/embedders/src/wasm_utils/validation.rs | 17 --------------- rs/embedders/tests/validation.rs | 26 +---------------------- rs/types/wasm_types/src/errors.rs | 19 ----------------- 3 files changed, 1 insertion(+), 61 deletions(-) diff --git a/rs/embedders/src/wasm_utils/validation.rs b/rs/embedders/src/wasm_utils/validation.rs index b369e49d836e..ca21ec9487b2 100644 --- a/rs/embedders/src/wasm_utils/validation.rs +++ b/rs/embedders/src/wasm_utils/validation.rs @@ -67,7 +67,6 @@ const WASM_FUNCTION_COMPLEXITY_LIMIT: Complexity = Complexity(1_000_000); pub const WASM_FUNCTION_SIZE_LIMIT: usize = 1_000_000; pub const MAX_CODE_SECTION_SIZE_IN_BYTES: u32 = 12 * 1024 * 1024; pub const MAX_WASM_FUNCTION_NAME_LENGTH: usize = 1024 * 1024; -pub const MAX_WASM_FUNCTION_NUM_LOCALS: usize = 2000; // Represents the expected function signature for any System APIs the Internet // Computer provides or any special exported user functions. @@ -1326,22 +1325,6 @@ fn validate_function_section( name: truncated_name, }); } - // Check number of locals - let num_locals = module - .functions - .get(id) - .kind() - .unwrap_local() /* we are looping over locals only */ - .unwrap() /* we retrieved the id from the same module */ - .body - .num_locals; - if num_locals > MAX_WASM_FUNCTION_NUM_LOCALS as u32 { - return Err(WasmValidationError::TooManyLocals { - index: *id as usize, - defined: num_locals as usize, - allowed: MAX_WASM_FUNCTION_NUM_LOCALS, - }); - } } Ok(()) diff --git a/rs/embedders/tests/validation.rs b/rs/embedders/tests/validation.rs index a6cb4a498e49..ac2743971983 100644 --- a/rs/embedders/tests/validation.rs +++ b/rs/embedders/tests/validation.rs @@ -7,8 +7,7 @@ use ic_embedders::{ wasm_utils::{ Complexity, WasmImportsDetails, WasmValidationDetails, validate_and_instrument_for_testing, validation::{ - MAX_WASM_FUNCTION_NAME_LENGTH, MAX_WASM_FUNCTION_NUM_LOCALS, RESERVED_SYMBOLS, - extract_custom_section_name, + MAX_WASM_FUNCTION_NAME_LENGTH, RESERVED_SYMBOLS, extract_custom_section_name, }, }, }; @@ -1479,26 +1478,3 @@ fn wasm_with_long_func_name_is_invalid() { }) ); } - -#[test] -fn wasm_with_many_locals_is_invalid() { - let wat = format!( - r#" - (module - (func $f (export "canister_update f") - (local {}) - ) - )"#, - "i32 ".repeat(MAX_WASM_FUNCTION_NUM_LOCALS + 1) - ); - - let wasm = wat2wasm(&wat).unwrap(); - assert_eq!( - validate_wasm_binary(&wasm, &EmbeddersConfig::default()), - Err(WasmValidationError::TooManyLocals { - index: 0, - defined: MAX_WASM_FUNCTION_NUM_LOCALS + 1, - allowed: MAX_WASM_FUNCTION_NUM_LOCALS, - }) - ); -} diff --git a/rs/types/wasm_types/src/errors.rs b/rs/types/wasm_types/src/errors.rs index 830025069623..6a655344ba0a 100644 --- a/rs/types/wasm_types/src/errors.rs +++ b/rs/types/wasm_types/src/errors.rs @@ -140,12 +140,6 @@ pub enum WasmValidationError { allowed: usize, name: String, }, - /// A function contains too many locals. - TooManyLocals { - index: usize, - defined: usize, - allowed: usize, - }, /// The code section is too large. CodeSectionTooLarge { size: u32, @@ -269,15 +263,6 @@ impl std::fmt::Display for WasmValidationError { "Wasm module contains a function at index {index} \ with name '{name}' of size {size} bytes that exceeds the maximum allowed size of {allowed} bytes.", ), - Self::TooManyLocals { - index, - defined, - allowed, - } => write!( - f, - "Wasm module contains a function at index {index} \ - with {defined} locals that exceeds the maximum allowed number of locals {allowed}" - ), Self::CodeSectionTooLarge { size, allowed } => write!( f, "Wasm module code section size of {size} \ @@ -355,10 +340,6 @@ impl AsErrorHelp for WasmValidationError { suggestion: "Try using shorter function names.".to_string(), doc_link: doc_ref("wasm-module-function-name-too-large"), }, - WasmValidationError::TooManyLocals { .. } => ErrorHelp::UserError { - suggestion: "Try different optimizer settings.".to_string(), - doc_link: doc_ref("wasm-module-too-many-locals"), - }, WasmValidationError::CodeSectionTooLarge { .. } => ErrorHelp::UserError { suggestion: "Try shrinking the module code section using tools like \ `ic-wasm` or splitting the logic across multiple canisters." From d9c18af9a1c0a553a1be6dc830b879ac477d8fff Mon Sep 17 00:00:00 2001 From: Leo Eichhorn <99166915+eichhorl@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:59:14 +0200 Subject: [PATCH 53/75] feat: CON-1467 CON-1705 Add registry version to `CanisterHttpRequestContext` (#10484) ## Background Currently, each node determines the registry version (thus committee) of an HTTP outcall by itself, depending on the current finalized height at the time of the request: https://github.com/dfinity/ic/blob/fe9e1b7a9a98ebafbf38230368226f4e928cc14f/rs/https_outcalls/consensus/src/pool_manager.rs#L577-L599 Additionally, HTTP shares with the wrong registry version are filtered out by the payload builder: https://github.com/dfinity/ic/blob/fe9e1b7a9a98ebafbf38230368226f4e928cc14f/rs/https_outcalls/consensus/src/payload_builder.rs#L212-L213 However, since the exact timing of when an HTTP request is handled on each replica may be different, the current finalized height (and therefore the registry version) may be different, as well. This has some adverse effects: 1. If the registry version changes after shares were produced, but before they were combined, the HTTP request will time out because shares with the old registry version are filtered out by the payload builder. 2. If a node produces some shares just before leaving the subnet, once these shares are received by its peers they might be invalidated, because to the rest of the subnet the node already left. 3. During membership changes, the HTTP outcalls committee will leave the subnet immediately without finishing outstanding requests first. ## Proposed Changes Instead, the PR proposes to add a registry version to the `CanisterHttpRequestContext`. In the future, this registry version could be used as the single source of truth for all nodes on the subnet, throughout the entire duration of the request. Additionally, during membership changes, this would allow the committee of the request to remain on the subnet until the request context disappears from the state. For compatibility reasons the registry version is still set to 0 (i.e. non-existent in the proto representation). Once this change is rolled out to all subnets, we can populate it with the actual registry version delivered to execution. In a subsequent release, we can then switch the source of truth to the new registry version in the context. --- rs/https_outcalls/client/src/client.rs | 8 ++++++-- .../consensus/benches/payload_validation.rs | 1 + .../consensus/src/payload_builder/tests.rs | 10 ++++++++++ rs/https_outcalls/consensus/src/pool_manager.rs | 1 + rs/protobuf/def/state/metadata/v1/metadata.proto | 1 + rs/protobuf/src/gen/state/state.metadata.v1.rs | 2 ++ rs/replicated_state/src/metadata_state/tests.rs | 3 ++- rs/types/types/src/canister_http.rs | 11 +++++++++++ 8 files changed, 34 insertions(+), 3 deletions(-) diff --git a/rs/https_outcalls/client/src/client.rs b/rs/https_outcalls/client/src/client.rs index 2c10c9859079..40b1f0a5ec23 100644 --- a/rs/https_outcalls/client/src/client.rs +++ b/rs/https_outcalls/client/src/client.rs @@ -560,8 +560,11 @@ mod tests { use ic_interfaces::execution_environment::{QueryExecutionError, QueryExecutionResponse}; use ic_logger::replica_logger::no_op_logger; use ic_test_utilities_types::messages::RequestBuilder; - use ic_types::canister_http::{ - MAX_CANISTER_HTTP_RESPONSE_BYTES, PricingVersion, RefundStatus, Replication, Transform, + use ic_types::{ + RegistryVersion, + canister_http::{ + MAX_CANISTER_HTTP_RESPONSE_BYTES, PricingVersion, RefundStatus, Replication, Transform, + }, }; use ic_types::{ canister_http::CanisterHttpMethod, messages::CallbackId, time::UNIX_EPOCH, @@ -654,6 +657,7 @@ mod tests { replication: Replication::FullyReplicated, pricing_version: PricingVersion::Legacy, refund_status: RefundStatus::default(), + registry_version: RegistryVersion::from(1), }, socks_proxy_addrs: vec![], } diff --git a/rs/https_outcalls/consensus/benches/payload_validation.rs b/rs/https_outcalls/consensus/benches/payload_validation.rs index 3e45e797b1e3..dfa276fa3642 100644 --- a/rs/https_outcalls/consensus/benches/payload_validation.rs +++ b/rs/https_outcalls/consensus/benches/payload_validation.rs @@ -492,6 +492,7 @@ fn request_context(replication: Replication) -> CanisterHttpRequestContext { replication, pricing_version: PricingVersion::Legacy, refund_status: RefundStatus::default(), + registry_version: RegistryVersion::from(1), } } diff --git a/rs/https_outcalls/consensus/src/payload_builder/tests.rs b/rs/https_outcalls/consensus/src/payload_builder/tests.rs index 2e959e90a5e6..c3922fa1d437 100644 --- a/rs/https_outcalls/consensus/src/payload_builder/tests.rs +++ b/rs/https_outcalls/consensus/src/payload_builder/tests.rs @@ -846,6 +846,7 @@ fn non_replicated_request_response_coming_in_gossip_payload_created() { replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), pricing_version: ic_types::canister_http::PricingVersion::Legacy, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; // Insert the context in the replicated state @@ -917,6 +918,7 @@ fn non_replicated_request_with_extra_share_includes_only_delegated_share() { replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), pricing_version: ic_types::canister_http::PricingVersion::Legacy, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; // Insert the context in the replicated state @@ -989,6 +991,7 @@ fn non_replicated_share_is_ignored_if_content_is_missing() { replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), pricing_version: ic_types::canister_http::PricingVersion::Legacy, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; inject_request_contexts(&mut payload_builder, [(callback_id, request_context)]); @@ -1039,6 +1042,7 @@ fn validate_payload_succeeds_for_valid_non_replicated_response() { replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), pricing_version: ic_types::canister_http::PricingVersion::Legacy, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; // Inject this context into the state reader used by the validator. @@ -1242,6 +1246,7 @@ fn validate_payload_fails_for_non_replicated_response_with_wrong_signer() { replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), pricing_version: ic_types::canister_http::PricingVersion::Legacy, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; // Inject this context into the state reader. @@ -1310,6 +1315,7 @@ fn validate_payload_fails_for_response_with_no_signatures() { replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), pricing_version: ic_types::canister_http::PricingVersion::Legacy, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; // Inject this context into the state reader used by the validator. @@ -1385,6 +1391,7 @@ fn validate_payload_fails_when_non_replicated_proof_is_for_fully_replicated_requ replication: ic_types::canister_http::Replication::FullyReplicated, pricing_version: ic_types::canister_http::PricingVersion::Legacy, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; // Inject this context into the state reader. @@ -1461,6 +1468,7 @@ fn validate_payload_fails_for_duplicate_non_replicated_response() { replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), pricing_version: ic_types::canister_http::PricingVersion::Legacy, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; // 2. Inject this context into the state reader @@ -4776,6 +4784,7 @@ pub(crate) fn request_context(replication: Replication) -> CanisterHttpRequestCo replication, pricing_version: ic_types::canister_http::PricingVersion::Legacy, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), } } @@ -4800,6 +4809,7 @@ fn flexible_request_context( }, pricing_version: ic_types::canister_http::PricingVersion::PayAsYouGo, refund_status: ic_types::canister_http::RefundStatus::default(), + registry_version: RegistryVersion::from(1), } } diff --git a/rs/https_outcalls/consensus/src/pool_manager.rs b/rs/https_outcalls/consensus/src/pool_manager.rs index 198105f8b807..62d1dc638c56 100644 --- a/rs/https_outcalls/consensus/src/pool_manager.rs +++ b/rs/https_outcalls/consensus/src/pool_manager.rs @@ -787,6 +787,7 @@ pub mod test { replication, pricing_version, refund_status: RefundStatus::default(), + registry_version: RegistryVersion::from(1), } } diff --git a/rs/protobuf/def/state/metadata/v1/metadata.proto b/rs/protobuf/def/state/metadata/v1/metadata.proto index 0ab9cd576e93..b7cf4fe1e07d 100644 --- a/rs/protobuf/def/state/metadata/v1/metadata.proto +++ b/rs/protobuf/def/state/metadata/v1/metadata.proto @@ -161,6 +161,7 @@ message CanisterHttpRequestContext { optional Replication replication = 11; optional PricingVersion pricing_version = 12; optional RefundStatus refund_status = 13; + uint64 registry_version = 14; reserved 5; } diff --git a/rs/protobuf/src/gen/state/state.metadata.v1.rs b/rs/protobuf/src/gen/state/state.metadata.v1.rs index d21e747282a7..1917084a06e9 100644 --- a/rs/protobuf/src/gen/state/state.metadata.v1.rs +++ b/rs/protobuf/src/gen/state/state.metadata.v1.rs @@ -227,6 +227,8 @@ pub struct CanisterHttpRequestContext { pub pricing_version: ::core::option::Option, #[prost(message, optional, tag = "13")] pub refund_status: ::core::option::Option, + #[prost(uint64, tag = "14")] + pub registry_version: u64, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct RefundStatus { diff --git a/rs/replicated_state/src/metadata_state/tests.rs b/rs/replicated_state/src/metadata_state/tests.rs index 6407dbca03e1..649004ae0630 100644 --- a/rs/replicated_state/src/metadata_state/tests.rs +++ b/rs/replicated_state/src/metadata_state/tests.rs @@ -39,7 +39,7 @@ use ic_types::crypto::canister_threshold_sig::idkg::{IDkgDealers, IDkgReceivers, use ic_types::ingress::WasmResult; use ic_types::messages::{CallbackId, CanisterCall, Payload, Refund, Request, RequestMetadata}; use ic_types::time::{CoarseTime, current_time}; -use ic_types::{ExecutionRound, Height}; +use ic_types::{ExecutionRound, Height, RegistryVersion}; use ic_types_cycles::{Cycles, NominalCyclesTesting}; use lazy_static::lazy_static; use maplit::btreemap; @@ -868,6 +868,7 @@ fn subnet_call_contexts_deserialization() { replication: Replication::FullyReplicated, pricing_version: PricingVersion::Legacy, refund_status: RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; subnet_call_context_manager.push_context(SubnetCallContext::CanisterHttpRequest( canister_http_request, diff --git a/rs/types/types/src/canister_http.rs b/rs/types/types/src/canister_http.rs index 20ef87a92f22..8a3377305bff 100644 --- a/rs/types/types/src/canister_http.rs +++ b/rs/types/types/src/canister_http.rs @@ -137,6 +137,8 @@ pub struct CanisterHttpRequestContext { pub replication: Replication, pub pricing_version: PricingVersion, pub refund_status: RefundStatus, + /// The registry version at which this request is being processed. + pub registry_version: RegistryVersion, } #[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)] @@ -281,6 +283,7 @@ impl From<&CanisterHttpRequestContext> for pb_metadata::CanisterHttpRequestConte replication: Some(replication_message), pricing_version: Some(pricing_message), refund_status: Some(refund_status), + registry_version: context.registry_version.get(), } } } @@ -405,6 +408,7 @@ impl TryFrom for CanisterHttpRequestCon replication, pricing_version, refund_status, + registry_version: RegistryVersion::from(context.registry_version), }) } } @@ -588,6 +592,8 @@ impl CanisterHttpRequestContext { refunded_cycles: Cycles::new(0), refunding_nodes: BTreeSet::new(), }, + // TODO: populate with the actual registry version this request is processed at. + registry_version: RegistryVersion::from(0), }) } @@ -695,6 +701,8 @@ impl CanisterHttpRequestContext { refunded_cycles: Cycles::new(0), refunding_nodes: BTreeSet::new(), }, + // TODO: populate with the actual registry version this request is processed at. + registry_version: RegistryVersion::from(0), }) } } @@ -1247,6 +1255,7 @@ mod tests { replication: Replication::FullyReplicated, pricing_version: PricingVersion::Legacy, refund_status: RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; let expected_size = context.url.len() @@ -1292,6 +1301,7 @@ mod tests { replication: Replication::FullyReplicated, pricing_version: PricingVersion::Legacy, refund_status: RefundStatus::default(), + registry_version: RegistryVersion::from(1), }; let expected_size = context.url.len() @@ -1371,6 +1381,7 @@ mod tests { refunded_cycles: Cycles::new(123), refunding_nodes: BTreeSet::from([node_test_id(1), node_test_id(2)]), }, + registry_version: RegistryVersion::from(7), }; let pb: pb_metadata::CanisterHttpRequestContext = (&initial).into(); From 3f387d7ffea89148392076481f01536fadb65cd1 Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Tue, 16 Jun 2026 18:17:48 +0200 Subject: [PATCH 54/75] chore: clarify motoko toolchains (#10480) This cleans up and documents where the motoko compiler comes from in Cargo and Bazel. --------- Co-authored-by: IDX GitHub Automation <> Co-authored-by: IDX GitHub Automation --- .devcontainer/devcontainer.json | 2 +- .github/workflows/api-bn-recovery-test.yml | 2 +- .github/workflows/ci-main.yml | 2 +- .github/workflows/ci-pr-only.yml | 2 +- .github/workflows/container-api-bn-recovery.yml | 2 +- .github/workflows/container-scan-nightly.yml | 2 +- .github/workflows/pocket-ic-tests-windows.yml | 2 +- .github/workflows/rate-limits-backend-release.yml | 2 +- .github/workflows/release-testing.yml | 2 +- .github/workflows/rosetta-release.yml | 2 +- .github/workflows/salt-sharing-canister-release.yml | 2 +- .github/workflows/schedule-daily.yml | 2 +- .github/workflows/schedule-rust-bench.yml | 2 +- .github/workflows/system-tests-benchmarks-nightly.yml | 2 +- .github/workflows/update-mainnet-canister-revisions.yaml | 2 +- MODULE.bazel | 2 +- ci/container/Dockerfile | 2 ++ ci/container/TAG | 2 +- rs/nns/handlers/lifeline/impl/build.rs | 4 ---- 19 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fdb6c4d212c1..bac981218653 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "image": "ghcr.io/dfinity/ic-dev@sha256:34f2713d09170e6def94ea81f1175af1505bb408162bcf3a613ac43b58a699e1", + "image": "ghcr.io/dfinity/ic-dev@sha256:668279aab052f00846e98c8cf9d37493d6b3e5ea10ffa4d0840b468219e1f44d", "remoteUser": "ubuntu", "privileged": true, "runArgs": [ diff --git a/.github/workflows/api-bn-recovery-test.yml b/.github/workflows/api-bn-recovery-test.yml index fc50053f67fe..1c6111fed8be 100644 --- a/.github/workflows/api-bn-recovery-test.yml +++ b/.github/workflows/api-bn-recovery-test.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 9306d2946076..0d9ecc46e0e3 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -33,7 +33,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/ci-pr-only.yml b/.github/workflows/ci-pr-only.yml index cfee71a972ec..6978173149fb 100644 --- a/.github/workflows/ci-pr-only.yml +++ b/.github/workflows/ci-pr-only.yml @@ -37,7 +37,7 @@ jobs: runs-on: &dind-small-setup labels: dind-small container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --mount type=tmpfs,target="/tmp/containers" steps: diff --git a/.github/workflows/container-api-bn-recovery.yml b/.github/workflows/container-api-bn-recovery.yml index a0984dd11d34..ae8e02d25f3a 100644 --- a/.github/workflows/container-api-bn-recovery.yml +++ b/.github/workflows/container-api-bn-recovery.yml @@ -28,7 +28,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/container-scan-nightly.yml b/.github/workflows/container-scan-nightly.yml index 404921677da0..18122156ffac 100644 --- a/.github/workflows/container-scan-nightly.yml +++ b/.github/workflows/container-scan-nightly.yml @@ -12,7 +12,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 60 diff --git a/.github/workflows/pocket-ic-tests-windows.yml b/.github/workflows/pocket-ic-tests-windows.yml index e73cfbd30c3f..fdeba78188a2 100644 --- a/.github/workflows/pocket-ic-tests-windows.yml +++ b/.github/workflows/pocket-ic-tests-windows.yml @@ -45,7 +45,7 @@ jobs: bazel-build-pocket-ic: name: Bazel Build PocketIC container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/rate-limits-backend-release.yml b/.github/workflows/rate-limits-backend-release.yml index 2f1f05b1beac..1cfdae770a5d 100644 --- a/.github/workflows/rate-limits-backend-release.yml +++ b/.github/workflows/rate-limits-backend-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/release-testing.yml b/.github/workflows/release-testing.yml index 10181548e6a7..aa48f1b2392e 100644 --- a/.github/workflows/release-testing.yml +++ b/.github/workflows/release-testing.yml @@ -35,7 +35,7 @@ jobs: group: dm1 labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 180 diff --git a/.github/workflows/rosetta-release.yml b/.github/workflows/rosetta-release.yml index 2bf60c222423..50a6d30cf0c2 100644 --- a/.github/workflows/rosetta-release.yml +++ b/.github/workflows/rosetta-release.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" environment: DockerHub diff --git a/.github/workflows/salt-sharing-canister-release.yml b/.github/workflows/salt-sharing-canister-release.yml index a83676ed5e79..a88e5bb8de5b 100644 --- a/.github/workflows/salt-sharing-canister-release.yml +++ b/.github/workflows/salt-sharing-canister-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/schedule-daily.yml b/.github/workflows/schedule-daily.yml index a8bec0dcb481..661098f10c54 100644 --- a/.github/workflows/schedule-daily.yml +++ b/.github/workflows/schedule-daily.yml @@ -14,7 +14,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/schedule-rust-bench.yml b/.github/workflows/schedule-rust-bench.yml index fedc50a34dc9..32deb5280b77 100644 --- a/.github/workflows/schedule-rust-bench.yml +++ b/.github/workflows/schedule-rust-bench.yml @@ -24,7 +24,7 @@ jobs: # see linux-x86-64 runner group labels: rust-benchmarks container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be # running on bare metal machine using ubuntu user options: --user ubuntu --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/system-tests-benchmarks-nightly.yml b/.github/workflows/system-tests-benchmarks-nightly.yml index 6ace65447f6b..9c79e0cb89ca 100644 --- a/.github/workflows/system-tests-benchmarks-nightly.yml +++ b/.github/workflows/system-tests-benchmarks-nightly.yml @@ -17,7 +17,7 @@ jobs: group: dm1 labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 480 diff --git a/.github/workflows/update-mainnet-canister-revisions.yaml b/.github/workflows/update-mainnet-canister-revisions.yaml index 4323c2980d69..11cfcd394e49 100644 --- a/.github/workflows/update-mainnet-canister-revisions.yaml +++ b/.github/workflows/update-mainnet-canister-revisions.yaml @@ -25,7 +25,7 @@ jobs: labels: dind-small environment: CREATE_PR container: - image: ghcr.io/dfinity/ic-build@sha256:5e821ef314fc3ce97bfa97903fb43bf9b42ad9c86e118b2a8446e482b85f1355 + image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" env: diff --git a/MODULE.bazel b/MODULE.bazel index 4e83a2429eca..e78debdc2234 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1279,4 +1279,4 @@ archive_override( ) motoko = use_extension("@rules_motoko//motoko:extensions.bzl", "motoko") -motoko.toolchain(version = "0.16.3") +motoko.toolchain(version = "0.16.3") # NOTE: keep version in line with Dockerfile diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile index d2a52f7b5f40..d53de14b2404 100644 --- a/ci/container/Dockerfile +++ b/ci/container/Dockerfile @@ -62,6 +62,8 @@ RUN curl -fsSL https://github.com/bazelbuild/bazelisk/releases/download/v1.28.1/ RUN ln -sf "$(command -v ld.lld)" "$(realpath /usr/bin/ld)" # Add motoko compiler +# This is only used by Cargo; Bazel pulls its own via rules_motoko +# NOTE: keep version in line with rules_motoko ARG motoko_version=0.16.3 RUN curl -fsSL https://github.com/dfinity/motoko/releases/download/${motoko_version}/motoko-linux-x86_64-${motoko_version}.tar.gz | tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/moc diff --git a/ci/container/TAG b/ci/container/TAG index 43f8dcd6bc94..7e62c208d040 100644 --- a/ci/container/TAG +++ b/ci/container/TAG @@ -1 +1 @@ -2d6069cd38193736e69b7edda7d96202452405a85670f82b3dc8674ac2ef9ebd +3cfb71e6fe1f5830eb736eda108e465dc52ee2917f36c3ac4a14bbc09fd07063 diff --git a/rs/nns/handlers/lifeline/impl/build.rs b/rs/nns/handlers/lifeline/impl/build.rs index b1c00746b6d2..89b645e8c6b8 100644 --- a/rs/nns/handlers/lifeline/impl/build.rs +++ b/rs/nns/handlers/lifeline/impl/build.rs @@ -148,8 +148,6 @@ fn main() { eprintln!( "The current directory is {:?}.\n\ `moc --version` output: {:?}.\n\ - IN_NIX_SHELL={:?}.\n\ - NIX_BUILD_TOP={:?}.\n\ lifeline.mo exists? {:?}.\n\ {:?} exists? {:?}.\n\ {:?} exists? {:?}.\n\ @@ -157,8 +155,6 @@ fn main() { `ls` output: {:?}.", env::current_dir().map(|pb| pb.as_path().display().to_string()), Command::new("moc").arg("--version").output(), - env::var("IN_NIX_SHELL"), - env::var("NIX_BUILD_TOP"), PathBuf::from_str("lifeline.mo").map(|pb| pb.as_path().is_file()), governance_out_did, governance_out_did.is_file(), From f2c0141bf4a7e9a6d4d390d419d2deb4fe9b6f5f Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:30:02 +0200 Subject: [PATCH 55/75] fix: II init args in PocketIC (#10496) --- rs/pocket_ic_server/src/external_canister_types.rs | 12 +++++++++++- rs/pocket_ic_server/src/pocket_ic.rs | 8 ++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/rs/pocket_ic_server/src/external_canister_types.rs b/rs/pocket_ic_server/src/external_canister_types.rs index 66eb3356558e..bbe8f165de3e 100644 --- a/rs/pocket_ic_server/src/external_canister_types.rs +++ b/rs/pocket_ic_server/src/external_canister_types.rs @@ -157,6 +157,14 @@ pub struct InternetIdentityFrontendInit { pub dev_csp: Option, } +#[derive(CandidType)] +pub struct SsoCredentialMigrationEntry { + pub discovery_domain: String, + pub issuer: String, + pub client_id: String, + pub name: Option, +} + #[derive(CandidType)] pub struct InternetIdentityInit { pub assigned_user_number_range: Option<(AnchorNumber, AnchorNumber)>, @@ -167,13 +175,15 @@ pub struct InternetIdentityInit { pub related_origins: Option>, pub new_flow_origins: Option>, pub openid_configs: Option>, + pub sso_discoverable_domains: Option>, + pub sso_credential_migration: Option>, pub analytics_config: Option>, pub enable_dapps_explorer: Option, pub is_production: Option, pub dummy_auth: Option>, pub backend_canister_id: Option, pub backend_origin: Option, - pub sso_discoverable_domains: Option>, + pub enable_dnssec_email_recovery: Option, pub dnssec_config: Option>, pub doh_config: Option>, } diff --git a/rs/pocket_ic_server/src/pocket_ic.rs b/rs/pocket_ic_server/src/pocket_ic.rs index c28e784b9ce3..c89b27b2dd73 100644 --- a/rs/pocket_ic_server/src/pocket_ic.rs +++ b/rs/pocket_ic_server/src/pocket_ic.rs @@ -2094,6 +2094,7 @@ impl PocketIcSubnets { // }; // }; // sso_discoverable_domains = null; + // sso_credential_migration = null; // archive_config = opt record { // polling_interval_ns = 15_000_000_000 : nat64; // entries_buffer_limit = 10_000 : nat64; @@ -2163,6 +2164,7 @@ impl PocketIcSubnets { // }; // }; // backend_origin = null; + // enable_dnssec_email_recovery = null; // captcha_config = opt record { // max_unsolved_captchas = 500 : nat64; // captcha_trigger = variant { Static = variant { CaptchaDisabled } }; @@ -2213,13 +2215,15 @@ impl PocketIcSubnets { related_origins: None, // DIFFERENT FROM ICP MAINNET new_flow_origins: None, // DIFFERENT FROM ICP MAINNET openid_configs: openid_google, // DIFFERENT FROM ICP MAINNET - analytics_config: None, // DIFFERENT FROM ICP MAINNET + sso_discoverable_domains: None, + sso_credential_migration: None, + analytics_config: None, // DIFFERENT FROM ICP MAINNET enable_dapps_explorer: Some(false), is_production: Some(false), // DIFFERENT FROM ICP MAINNET dummy_auth: Some(Some(dummy_auth_config)), // DIFFERENT FROM ICP MAINNET backend_canister_id: Some(IDENTITY_CANISTER_ID.get().0), backend_origin: None, - sso_discoverable_domains: None, + enable_dnssec_email_recovery: None, dnssec_config: None, // DIFFERENT FROM ICP MAINNET doh_config: None, // DIFFERENT FROM ICP MAINNET }); From 5db4f9ec200eeb9eb9ece154a400e5b7e6b59ede Mon Sep 17 00:00:00 2001 From: Leo Eichhorn <99166915+eichhorl@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:49:11 +0200 Subject: [PATCH 56/75] feat: Add delivered HTTP call contexts to subnet call context manager (#10489) Currently, all HTTP request contexts are stored in the same collection of the subnet call context manager. They are removed once consensus delivers a response to the canister. In the future, there will be a second "asynchronous refund" phase as part of the context's life cycle: After consensus delivers the HTTP response and an _initial_ refund, consensus may continue to deliver _asynchronous_ refunds, that correspond to shares which didn't make it into the original response. To do this, the context needs to persist even after a response was delivered. This PR proposes to support this by introducing a second collection, which will hold such delivered contexts. Whenever a response is delivered, the corresponding context is moved into the new collection. An alternative would have been to mark the context as "delivered", e.g. by setting a boolean flag. However, introducing a second collection has some natural advantages: 1. The size of the collection holding undelivered contexts is limited. Including delivered contexts in the same collection would therefore adversely affect throughput. 2. Some places will only want to look at undelivered contexts (I.e. response share aggregation & validation). 3. Some places will only want to look at delivered contexts (I.e. async refund creation & validation) 4. Some places will want to look at both collections (I.e share validation & purging), for which we can simply take the union of both collections. Note that for compatibility reasons, the new collection is still unused. --- .../def/state/metadata/v1/metadata.proto | 1 + .../src/gen/state/state.metadata.v1.rs | 3 +++ .../subnet_call_context_manager.rs | 4 ++++ .../subnet_call_context_manager/proto.rs | 22 +++++++++++++++++++ 4 files changed, 30 insertions(+) diff --git a/rs/protobuf/def/state/metadata/v1/metadata.proto b/rs/protobuf/def/state/metadata/v1/metadata.proto index b7cf4fe1e07d..8ea570b59984 100644 --- a/rs/protobuf/def/state/metadata/v1/metadata.proto +++ b/rs/protobuf/def/state/metadata/v1/metadata.proto @@ -307,6 +307,7 @@ message SubnetCallContextManager { repeated ReshareChainKeyContextTree reshare_chain_key_contexts = 17; repeated SignWithThresholdContextTree sign_with_threshold_contexts = 18; repeated PreSignatureStashTree pre_signature_stashes = 19; + repeated CanisterHttpRequestContextTree delivered_canister_http_request_contexts = 20; } message SubnetMetrics { diff --git a/rs/protobuf/src/gen/state/state.metadata.v1.rs b/rs/protobuf/src/gen/state/state.metadata.v1.rs index 1917084a06e9..4bf44c20e8c6 100644 --- a/rs/protobuf/src/gen/state/state.metadata.v1.rs +++ b/rs/protobuf/src/gen/state/state.metadata.v1.rs @@ -455,6 +455,9 @@ pub struct SubnetCallContextManager { pub sign_with_threshold_contexts: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag = "19")] pub pre_signature_stashes: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "20")] + pub delivered_canister_http_request_contexts: + ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct SubnetMetrics { diff --git a/rs/replicated_state/src/metadata_state/subnet_call_context_manager.rs b/rs/replicated_state/src/metadata_state/subnet_call_context_manager.rs index e0d904d89fa3..30fdbefff2fe 100644 --- a/rs/replicated_state/src/metadata_state/subnet_call_context_manager.rs +++ b/rs/replicated_state/src/metadata_state/subnet_call_context_manager.rs @@ -217,6 +217,9 @@ pub struct SubnetCallContextManager { pub setup_initial_dkg_contexts: BTreeMap, pub sign_with_threshold_contexts: BTreeMap, pub canister_http_request_contexts: BTreeMap, + /// `CanisterHttpRequestContext`s whose responses have already been delivered to execution. + /// They are kept here such that asynchronous refunds may continue to be processed. + pub delivered_canister_http_request_contexts: BTreeMap, pub reshare_chain_key_contexts: BTreeMap, pub bitcoin_get_successors_contexts: BTreeMap, pub bitcoin_send_transaction_internal_contexts: @@ -743,6 +746,7 @@ mod testing { setup_initial_dkg_contexts: Default::default(), sign_with_threshold_contexts: Default::default(), canister_http_request_contexts: Default::default(), + delivered_canister_http_request_contexts: Default::default(), reshare_chain_key_contexts: Default::default(), bitcoin_get_successors_contexts: Default::default(), bitcoin_send_transaction_internal_contexts: Default::default(), diff --git a/rs/replicated_state/src/metadata_state/subnet_call_context_manager/proto.rs b/rs/replicated_state/src/metadata_state/subnet_call_context_manager/proto.rs index 5e7aea110d45..9ddd37b0b10c 100644 --- a/rs/replicated_state/src/metadata_state/subnet_call_context_manager/proto.rs +++ b/rs/replicated_state/src/metadata_state/subnet_call_context_manager/proto.rs @@ -59,6 +59,16 @@ impl From<&SubnetCallContextManager> for pb_metadata::SubnetCallContextManager { }, ) .collect(), + delivered_canister_http_request_contexts: item + .delivered_canister_http_request_contexts + .iter() + .map( + |(callback_id, context)| pb_metadata::CanisterHttpRequestContextTree { + callback_id: callback_id.get(), + context: Some(context.into()), + }, + ) + .collect(), bitcoin_get_successors_contexts: item .bitcoin_get_successors_contexts .iter() @@ -188,6 +198,17 @@ impl TryFrom<(Time, pb_metadata::SubnetCallContextManager)> for SubnetCallContex canister_http_request_contexts.insert(CallbackId::new(entry.callback_id), context); } + let mut delivered_canister_http_request_contexts = + BTreeMap::::new(); + for entry in item.delivered_canister_http_request_contexts { + let context: CanisterHttpRequestContext = try_from_option_field( + entry.context, + "SystemMetadata::DeliveredCanisterHttpRequestContext", + )?; + delivered_canister_http_request_contexts + .insert(CallbackId::new(entry.callback_id), context); + } + let mut reshare_chain_key_contexts = BTreeMap::::new(); for entry in item.reshare_chain_key_contexts { let pb_context = @@ -261,6 +282,7 @@ impl TryFrom<(Time, pb_metadata::SubnetCallContextManager)> for SubnetCallContex setup_initial_dkg_contexts, sign_with_threshold_contexts, canister_http_request_contexts, + delivered_canister_http_request_contexts, bitcoin_get_successors_contexts, bitcoin_send_transaction_internal_contexts, canister_management_calls: CanisterManagementCalls { From c78a8f3451202d8b7a256a1c488b264711c4b48e Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:52:31 +0200 Subject: [PATCH 57/75] refactor: introduce CyclesAccountManagerSubnetConfig (#10485) This PR introduces the type `CyclesAccountManagerSubnetConfig` bundling subnet size and cost schedule needed for cycles computation. The motivation for this change is to easily add a new parameter (reference subnet size) in a follow-up PR and have it threaded through the stack transparently. --------- Co-authored-by: Claude Sonnet 4.6 --- Cargo.lock | 3 + rs/canister_sandbox/src/protocol/sbxsvc.rs | 10 +- rs/canister_sandbox/src/sandbox_server.rs | 10 +- .../src/cycles_account_manager.rs | 322 +++++++----------- .../src/cycles_account_manager/tests.rs | 63 ++-- rs/cycles_account_manager/src/lib.rs | 1 + .../tests/cycles_account_manager.rs | 227 +++++------- rs/embedders/BUILD.bazel | 1 + rs/embedders/fuzz/BUILD.bazel | 1 + rs/embedders/fuzz/Cargo.toml | 1 + rs/embedders/fuzz/src/wasm_executor.rs | 8 +- .../src/wasmtime_embedder/system_api.rs | 36 +- .../system_api/sandbox_safe_system_state.rs | 84 ++--- rs/embedders/src/wasmtime_embedder/tests.rs | 8 +- rs/embedders/tests/common/mod.rs | 10 +- .../tests/sandbox_safe_system_state.rs | 122 +++---- rs/embedders/tests/system_api.rs | 26 +- .../tests/wasmtime_random_memory_writes.rs | 8 +- rs/execution_environment/BUILD.bazel | 6 + .../system_api/execute_inspect_message.rs | 7 +- .../benches/system_api/execute_query.rs | 7 +- .../benches/system_api/execute_update.rs | 7 +- .../benches/wasm_instructions/main.rs | 7 +- rs/execution_environment/src/canister_logs.rs | 17 +- .../src/canister_manager.rs | 164 ++++----- .../src/canister_manager/tests.rs | 97 ++---- .../src/execution/call_or_task.rs | 21 +- .../src/execution/call_or_task/tests.rs | 17 +- .../src/execution/inspect_message.rs | 6 +- .../src/execution/install.rs | 4 +- .../src/execution/install_code.rs | 15 +- .../src/execution/install_code/tests.rs | 26 +- .../src/execution/nonreplicated_query.rs | 7 +- .../src/execution/response.rs | 34 +- .../src/execution/response/tests.rs | 23 +- .../src/execution/upgrade.rs | 6 +- .../src/execution/upgrade/tests.rs | 8 +- .../src/execution_environment.rs | 216 ++++-------- .../src/execution_environment/tests.rs | 4 +- .../tests/canister_snapshots.rs | 14 +- .../tests/compilation.rs | 8 +- rs/execution_environment/src/hypervisor.rs | 11 +- rs/execution_environment/src/query_handler.rs | 3 +- .../src/query_handler/query_context.rs | 31 +- rs/execution_environment/src/scheduler.rs | 34 +- .../src/scheduler/test_utilities.rs | 51 ++- rs/execution_environment/tests/hypervisor.rs | 48 +-- rs/ingress_manager/src/ingress_selector.rs | 15 +- rs/messaging/src/scheduling/valid_set_rule.rs | 15 +- .../src/scheduling/valid_set_rule/test.rs | 74 +--- rs/replicated_state/src/replicated_state.rs | 11 +- rs/state_machine_tests/src/lib.rs | 6 +- rs/test_utilities/embedders/BUILD.bazel | 1 + rs/test_utilities/embedders/Cargo.toml | 1 + rs/test_utilities/embedders/src/lib.rs | 8 +- .../execution_environment/src/lib.rs | 107 +++--- .../canister_lifecycle.rs | 8 +- rs/tests/networking/BUILD.bazel | 1 + rs/tests/networking/Cargo.toml | 1 + .../canister_http_correctness_test.rs | 4 +- .../cycles_account_manager_subnet_config.rs | 18 + rs/types/cycles/src/lib.rs | 2 + 62 files changed, 878 insertions(+), 1234 deletions(-) create mode 100644 rs/types/cycles/src/cycles_account_manager_subnet_config.rs diff --git a/Cargo.lock b/Cargo.lock index e2c28b4eaa30..c2e1fc00702c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15164,6 +15164,7 @@ dependencies = [ "ic-cycles-account-manager", "ic-embedders", "ic-interfaces", + "ic-limits", "ic-logger", "ic-management-canister-types-private", "ic-registry-subnet-type", @@ -18462,6 +18463,7 @@ dependencies = [ "ic-certification 0.9.0", "ic-crypto-tree-hash", "ic-crypto-utils-threshold-sig-der", + "ic-cycles-account-manager", "ic-http-endpoints-public", "ic-http-endpoints-test-agent", "ic-limits", @@ -25919,6 +25921,7 @@ dependencies = [ "ic-cycles-account-manager", "ic-embedders", "ic-interfaces", + "ic-limits", "ic-logger", "ic-management-canister-types-private", "ic-metrics", diff --git a/rs/canister_sandbox/src/protocol/sbxsvc.rs b/rs/canister_sandbox/src/protocol/sbxsvc.rs index 4906bff27643..fc4fab734fc0 100644 --- a/rs/canister_sandbox/src/protocol/sbxsvc.rs +++ b/rs/canister_sandbox/src/protocol/sbxsvc.rs @@ -319,7 +319,9 @@ mod tests { use ic_base_types::NumSeconds; use ic_config::subnet_config::{CyclesAccountManagerConfig, SubnetSecurity}; - use ic_cycles_account_manager::{CyclesAccountManager, ResourceSaturation}; + use ic_cycles_account_manager::{ + CyclesAccountManager, CyclesAccountManagerSubnetConfig, ResourceSaturation, + }; use ic_embedders::wasmtime_embedder::system_api::{ ApiType, ExecutionParameters, InstructionLimits, sandbox_safe_system_state::SandboxSafeSystemState, @@ -327,6 +329,7 @@ mod tests { use ic_interfaces::execution_environment::{ ExecutionMode, MessageMemoryUsage, SubnetAvailableMemory, }; + use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_registry_subnet_type::SubnetType; use ic_replicated_state::{Memory, NetworkTopology, NumWasmPages, PageMap, SystemState}; use ic_test_utilities_types::ids::canister_test_id; @@ -499,7 +502,10 @@ mod tests { Some(canister_test_id(1).get()), Some(CallContextId::new(123)), IS_WASM64_EXECUTION, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ), wasm_reserved_pages: NumWasmPages::new(1), }, diff --git a/rs/canister_sandbox/src/sandbox_server.rs b/rs/canister_sandbox/src/sandbox_server.rs index c7d926c5cde2..470e60266f9b 100644 --- a/rs/canister_sandbox/src/sandbox_server.rs +++ b/rs/canister_sandbox/src/sandbox_server.rs @@ -127,7 +127,9 @@ mod tests { use ic_base_types::{NumSeconds, PrincipalId}; use ic_config::embedders::Config as EmbeddersConfig; use ic_config::subnet_config::{CyclesAccountManagerConfig, SchedulerConfig, SubnetSecurity}; - use ic_cycles_account_manager::{CyclesAccountManager, ResourceSaturation}; + use ic_cycles_account_manager::{ + CyclesAccountManager, CyclesAccountManagerSubnetConfig, ResourceSaturation, + }; use ic_embedders::{ SerializedModuleBytes, WasmtimeEmbedder, wasm_utils, wasmtime_embedder::system_api::{ @@ -213,8 +215,10 @@ mod tests { BTreeMap::new(), 0, ic00_aliases, - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), SchedulerConfig::application_subnet().dirty_page_overhead, CanisterTimer::Inactive, 0, diff --git a/rs/cycles_account_manager/src/cycles_account_manager.rs b/rs/cycles_account_manager/src/cycles_account_manager.rs index f5c3344c6046..afb10e06c0ab 100644 --- a/rs/cycles_account_manager/src/cycles_account_manager.rs +++ b/rs/cycles_account_manager/src/cycles_account_manager.rs @@ -17,9 +17,10 @@ use ic_types::{ messages::{MAX_INTER_CANISTER_PAYLOAD_IN_BYTES, Payload, SignedIngress}, }; use ic_types_cycles::{ - CanisterCreation, CanisterCyclesCostSchedule, CompoundCycles, Cycles, CyclesUseCase, - CyclesUseCaseKind, DeletedCanisters, ECDSAOutcalls, HTTPOutcalls, IngressInduction, - Instructions, Memory, RequestAndResponseTransmission, SchnorrOutcalls, VetKd, + CanisterCreation, CanisterCyclesCostSchedule, CompoundCycles, Cycles, + CyclesAccountManagerSubnetConfig, CyclesUseCase, CyclesUseCaseKind, DeletedCanisters, + ECDSAOutcalls, HTTPOutcalls, IngressInduction, Instructions, Memory, + RequestAndResponseTransmission, SchnorrOutcalls, VetKd, }; use prometheus::IntCounter; use serde::{Deserialize, Serialize}; @@ -100,15 +101,15 @@ impl CyclesAccountManager { fn scale_cost( &self, cycles: Cycles, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { debug_assert_ne!( self.config.reference_subnet_size, 0, "prevent divide by zero panic" ); - let real = (cycles * subnet_size) / self.config.reference_subnet_size.max(1); - CompoundCycles::::new(real, cost_schedule) + let real = + (cycles * subnet_cycles_config.subnet_size) / self.config.reference_subnet_size.max(1); + CompoundCycles::::new(real, subnet_cycles_config.cost_schedule) } //////////////////////////////////////////////////////////////////////////// @@ -120,84 +121,63 @@ impl CyclesAccountManager { /// Returns the fee to create a canister in [`Cycles`]. pub fn canister_creation_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.scale_cost( - self.config.canister_creation_fee, - subnet_size, - cost_schedule, - ) + self.scale_cost(self.config.canister_creation_fee, subnet_cycles_config) } /// Returns the fee for receiving an ingress message in [`Cycles`]. pub fn ingress_message_received_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { self.scale_cost( self.config.ingress_message_reception_fee, - subnet_size, - cost_schedule, + subnet_cycles_config, ) } /// Returns the fee for storing a GiB of data per second scaled by subnet size. pub fn gib_storage_per_second_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.scale_cost( - self.config.gib_storage_per_second_fee, - subnet_size, - cost_schedule, - ) + self.scale_cost(self.config.gib_storage_per_second_fee, subnet_cycles_config) } /// Returns the base fee per second charged to every canister that has any memory usage. pub fn base_per_second_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.scale_cost(self.config.base_per_second_fee, subnet_size, cost_schedule) + self.scale_cost(self.config.base_per_second_fee, subnet_cycles_config) } /// Returns the fee per byte of ingress message received in [`Cycles`]. pub fn ingress_byte_received_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.scale_cost( - self.config.ingress_byte_reception_fee, - subnet_size, - cost_schedule, - ) + self.scale_cost(self.config.ingress_byte_reception_fee, subnet_cycles_config) } /// Returns the fee for performing a xnet call in [`Cycles`]. pub fn xnet_call_performed_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.scale_cost(self.config.xnet_call_fee, subnet_size, cost_schedule) + self.scale_cost(self.config.xnet_call_fee, subnet_cycles_config) } /// Returns the fee per byte of transmitted xnet call in [`Cycles`]. pub fn xnet_call_bytes_transmitted_fee( &self, payload_size: NumBytes, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { self.scale_cost( self.config.xnet_byte_transmission_fee * payload_size.get(), - subnet_size, - cost_schedule, + subnet_cycles_config, ) } @@ -208,16 +188,14 @@ impl CyclesAccountManager { memory_usage: NumBytes, message_memory_usage: MessageMemoryUsage, compute_allocation: ComputeAllocation, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> Cycles { let cycles_burn_rate = self.idle_cycles_burned_rate_by_resource( memory_allocation, memory_usage, message_memory_usage, compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, ); cycles_burn_rate.total() } @@ -230,25 +208,22 @@ impl CyclesAccountManager { memory_usage: NumBytes, message_memory_usage: MessageMemoryUsage, compute_allocation: ComputeAllocation, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CyclesBurnedRate { let memory = memory_allocation.allocated_bytes(memory_usage); CyclesBurnedRate { - memory: self.memory_cost(memory, DAY, subnet_size, cost_schedule) - + self.canister_base_cost(memory, DAY, subnet_size, cost_schedule), + memory: self.memory_cost(memory, DAY, subnet_cycles_config) + + self.canister_base_cost(memory, DAY, subnet_cycles_config), message_memory: self.memory_cost( message_memory_usage.total(), DAY, - subnet_size, - cost_schedule, + subnet_cycles_config, ), compute_allocation: self.compute_allocation_cost( compute_allocation, DAY, - subnet_size, - cost_schedule, + subnet_cycles_config, ), } } @@ -262,8 +237,7 @@ impl CyclesAccountManager { memory_usage: NumBytes, message_memory_usage: MessageMemoryUsage, compute_allocation: ComputeAllocation, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, reserved_balance: Cycles, ) -> Cycles { let idle_cycles_burned_rate = self.idle_cycles_burned_rate( @@ -271,8 +245,7 @@ impl CyclesAccountManager { memory_usage, message_memory_usage, compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, ); let threshold = idle_cycles_burned_rate * freeze_threshold.get() as u128 / SECONDS_PER_DAY; @@ -305,8 +278,7 @@ impl CyclesAccountManager { canister_compute_allocation: ComputeAllocation, cycles_balance: &mut Cycles, cycles: Cycles, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, reserved_balance: Cycles, reveal_top_up: bool, ) -> Result<(), CanisterOutOfCyclesError> { @@ -320,8 +292,7 @@ impl CyclesAccountManager { canister_current_memory_usage, canister_current_message_memory_usage, canister_compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, reserved_balance, ), reveal_top_up, @@ -344,8 +315,7 @@ impl CyclesAccountManager { canister_current_message_memory_usage: MessageMemoryUsage, canister_compute_allocation: ComputeAllocation, cycles: Cycles, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, reveal_top_up: bool, ) -> Result<(), CanisterOutOfCyclesError> { let threshold = self.freeze_threshold_cycles( @@ -354,8 +324,7 @@ impl CyclesAccountManager { canister_current_memory_usage, canister_current_message_memory_usage, canister_compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, canister.system_state.reserved_balance(), ); if canister.has_paused_execution_or_install_code() { @@ -375,7 +344,7 @@ impl CyclesAccountManager { } else { self.consume_with_threshold::( &mut canister.system_state, - CompoundCycles::new(cycles, cost_schedule), + CompoundCycles::new(cycles, subnet_cycles_config.cost_schedule), threshold, reveal_top_up, ) @@ -398,8 +367,7 @@ impl CyclesAccountManager { canister_current_memory_usage: NumBytes, canister_current_message_memory_usage: MessageMemoryUsage, cycles: CompoundCycles, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, reveal_top_up: bool, ) -> Result<(), CanisterOutOfCyclesError> { let threshold = self.freeze_threshold_cycles( @@ -408,8 +376,7 @@ impl CyclesAccountManager { canister_current_memory_usage, canister_current_message_memory_usage, system_state.compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), ); self.consume_with_threshold(system_state, cycles, threshold, reveal_top_up) @@ -422,21 +389,19 @@ impl CyclesAccountManager { sender: &PrincipalId, canister: &mut CanisterState, amount: NumInstructions, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, execution_mode: WasmExecutionMode, ) -> Result<(), CanisterOutOfCyclesError> { let memory_usage = canister.memory_usage(); let message_memory = canister.message_memory_usage(); - let cycles = self.execution_cost(amount, subnet_size, cost_schedule, execution_mode); + let cycles = self.execution_cost(amount, subnet_cycles_config, execution_mode); let reveal_top_up = canister.controllers().contains(sender); self.consume_cycles( &mut canister.system_state, memory_usage, message_memory, cycles, - subnet_size, - cost_schedule, + subnet_cycles_config, reveal_top_up, ) } @@ -448,20 +413,18 @@ impl CyclesAccountManager { sender: &PrincipalId, canister: &mut CanisterState, amount: NumInstructions, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> Result<(), CanisterOutOfCyclesError> { let memory_usage = canister.memory_usage(); let message_memory = canister.message_memory_usage(); - let cycles = self.management_canister_cost(amount, subnet_size, cost_schedule); + let cycles = self.management_canister_cost(amount, subnet_cycles_config); let reveal_top_up = canister.controllers().contains(sender); self.consume_cycles( &mut canister.system_state, memory_usage, message_memory, cycles, - subnet_size, - cost_schedule, + subnet_cycles_config, reveal_top_up, ) } @@ -483,13 +446,11 @@ impl CyclesAccountManager { canister_current_message_memory_usage: MessageMemoryUsage, canister_compute_allocation: ComputeAllocation, num_instructions: NumInstructions, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, reveal_top_up: bool, execution_mode: WasmExecutionMode, ) -> Result, CanisterOutOfCyclesError> { - let cost = - self.execution_cost(num_instructions, subnet_size, cost_schedule, execution_mode); + let cost = self.execution_cost(num_instructions, subnet_cycles_config, execution_mode); self.consume_with_threshold( system_state, cost, @@ -499,8 +460,7 @@ impl CyclesAccountManager { canister_current_memory_usage, canister_current_message_memory_usage, canister_compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), ), reveal_top_up, @@ -517,15 +477,14 @@ impl CyclesAccountManager { &self, canister: &CanisterState, max_instructions: NumInstructions, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> Result<(), CanisterOutOfCyclesError> { let execution_mode = canister .execution_state .as_ref() .map_or(WasmExecutionMode::Wasm32, |es| es.wasm_execution_mode); let execution_cost = - self.execution_cost(max_instructions, subnet_size, cost_schedule, execution_mode); + self.execution_cost(max_instructions, subnet_cycles_config, execution_mode); self.can_withdraw_cycles_with_threshold( &canister.system_state, @@ -533,8 +492,7 @@ impl CyclesAccountManager { canister.memory_usage(), canister.message_memory_usage(), canister.system_state.reserved_balance(), - subnet_size, - cost_schedule, + subnet_cycles_config, false, ) } @@ -548,8 +506,7 @@ impl CyclesAccountManager { num_instructions_initially_charged: NumInstructions, prepaid_execution_cycles: CompoundCycles, error_counter: &IntCounter, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, execution_mode: WasmExecutionMode, log: &ReplicaLogger, ) { @@ -569,8 +526,7 @@ impl CyclesAccountManager { let cycles_to_refund = self .scale_cost( self.convert_instructions_to_cycles(num_instructions_to_refund, execution_mode), - subnet_size, - cost_schedule, + subnet_cycles_config, ) .min(prepaid_execution_cycles); system_state.refund_cycles(prepaid_execution_cycles, cycles_to_refund); @@ -582,13 +538,12 @@ impl CyclesAccountManager { &self, compute_allocation: ComputeAllocation, duration: Duration, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { let cycles = self.config.compute_percent_allocated_per_second_fee * duration.as_secs() * compute_allocation.as_percent(); - self.scale_cost(cycles, subnet_size, cost_schedule) + self.scale_cost(cycles, subnet_cycles_config) } /// Computes the cost of inducting an ingress message. @@ -600,8 +555,7 @@ impl CyclesAccountManager { &self, ingress: &SignedIngress, effective_canister_id: Option, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> IngressInductionCost { let raw_bytes = NumBytes::from(ingress.binary().len() as u64); let ingress = ingress.content(); @@ -627,8 +581,7 @@ impl CyclesAccountManager { match paying_canister { Some(paying_canister) => { - let cost = - self.ingress_induction_cost_from_bytes(raw_bytes, subnet_size, cost_schedule); + let cost = self.ingress_induction_cost_from_bytes(raw_bytes, subnet_cycles_config); IngressInductionCost::Fee { payer: paying_canister, cost: cost.real(), @@ -642,11 +595,10 @@ impl CyclesAccountManager { pub fn ingress_induction_cost_from_bytes( &self, bytes: NumBytes, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.ingress_message_received_fee(subnet_size, cost_schedule) - + self.ingress_byte_received_fee(subnet_size, cost_schedule) * bytes.get() + self.ingress_message_received_fee(subnet_cycles_config) + + self.ingress_byte_received_fee(subnet_cycles_config) * bytes.get() } /// How often canisters should be charged for memory and compute allocation. @@ -657,32 +609,25 @@ impl CyclesAccountManager { /// Amount to charge for an ECDSA signature. pub fn ecdsa_signature_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.scale_cost(self.config.ecdsa_signature_fee, subnet_size, cost_schedule) + self.scale_cost(self.config.ecdsa_signature_fee, subnet_cycles_config) } /// Amount to charge for a Schnorr signature. pub fn schnorr_signature_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.scale_cost( - self.config.schnorr_signature_fee, - subnet_size, - cost_schedule, - ) + self.scale_cost(self.config.schnorr_signature_fee, subnet_cycles_config) } /// Amount to charge for vet KD. pub fn vetkd_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.scale_cost(self.config.vetkd_fee, subnet_size, cost_schedule) + self.scale_cost(self.config.vetkd_fee, subnet_cycles_config) } //////////////////////////////////////////////////////////////////////////// @@ -697,8 +642,7 @@ impl CyclesAccountManager { &self, bytes: NumBytes, duration: Duration, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { let one_gib = 1024 * 1024 * 1024; let cycles = Cycles::from( @@ -707,22 +651,21 @@ impl CyclesAccountManager { * duration.as_secs() as u128) / one_gib, ); - self.scale_cost(cycles, subnet_size, cost_schedule) + self.scale_cost(cycles, subnet_cycles_config) } pub fn canister_base_cost( &self, bytes: NumBytes, duration: Duration, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { let cycles = if bytes > NumBytes::new(0) { self.config.base_per_second_fee * duration.as_secs() as u128 } else { Cycles::zero() }; - self.scale_cost(cycles, subnet_size, cost_schedule) + self.scale_cost(cycles, subnet_cycles_config) } /// Returns the amount of reserved cycles required for allocating the given @@ -731,8 +674,7 @@ impl CyclesAccountManager { &self, allocated_bytes: NumBytes, storage_saturation: &ResourceSaturation, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { // The reservation cycles for `allocated_bytes` can be computed as // the difference between @@ -740,9 +682,8 @@ impl CyclesAccountManager { // - the total reservation cycles from 0 to `usage`. self.total_storage_reservation_cycles( &storage_saturation.add(allocated_bytes.get()), - subnet_size, - cost_schedule, - ) - self.total_storage_reservation_cycles(storage_saturation, subnet_size, cost_schedule) + subnet_cycles_config, + ) - self.total_storage_reservation_cycles(storage_saturation, subnet_cycles_config) } /// Returns the total amount of reserved cycles for the given resource @@ -752,8 +693,7 @@ impl CyclesAccountManager { fn total_storage_reservation_cycles( &self, storage_saturation: &ResourceSaturation, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { let duration = Duration::from_secs( storage_saturation @@ -766,8 +706,7 @@ impl CyclesAccountManager { self.memory_cost( NumBytes::new(storage_saturation.usage_above_threshold()), duration / 2, - subnet_size, - cost_schedule, + subnet_cycles_config, ) } @@ -805,8 +744,7 @@ impl CyclesAccountManager { canister_compute_allocation: ComputeAllocation, prepayment_for_response_execution: CompoundCycles, prepayment_for_call_transmission: CompoundCycles, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, reserved_balance: Cycles, reveal_top_up: bool, ) -> Result<(), CanisterOutOfCyclesError> { @@ -826,8 +764,7 @@ impl CyclesAccountManager { canister_current_memory_usage, canister_current_message_memory_usage, canister_compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, reserved_balance, ), reveal_top_up, @@ -841,12 +778,11 @@ impl CyclesAccountManager { pub fn xnet_total_transmission_fee( &self, payload_size: NumBytes, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - self.xnet_call_performed_fee(subnet_size, cost_schedule) - + self.xnet_call_bytes_transmitted_fee(payload_size, subnet_size, cost_schedule) - + self.prepayment_for_response_transmission(subnet_size, cost_schedule) + self.xnet_call_performed_fee(subnet_cycles_config) + + self.xnet_call_bytes_transmitted_fee(payload_size, subnet_cycles_config) + + self.prepayment_for_response_transmission(subnet_cycles_config) } /// The total fee for an xnet call, including payload size, transmission (both ways) @@ -856,14 +792,13 @@ impl CyclesAccountManager { pub fn xnet_call_total_fee( &self, payload_size: NumBytes, - subnet_size: usize, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, execution_mode: WasmExecutionMode, - cost_schedule: CanisterCyclesCostSchedule, ) -> Cycles { // response execution might be free depending on cost_schedule let prepayment_for_response_execution = - self.prepayment_for_response_execution(subnet_size, cost_schedule, execution_mode); - self.xnet_total_transmission_fee(payload_size, subnet_size, cost_schedule) + self.prepayment_for_response_execution(subnet_cycles_config, execution_mode); + self.xnet_total_transmission_fee(payload_size, subnet_cycles_config) .real() + prepayment_for_response_execution.real() } @@ -872,14 +807,12 @@ impl CyclesAccountManager { /// response callback. pub fn prepayment_for_response_execution( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, execution_mode: WasmExecutionMode, ) -> CompoundCycles { self.execution_cost( self.max_num_instructions, - subnet_size, - cost_schedule, + subnet_cycles_config, execution_mode, ) } @@ -888,13 +821,11 @@ impl CyclesAccountManager { /// response message. pub fn prepayment_for_response_transmission( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { self.scale_cost( self.config.xnet_byte_transmission_fee * MAX_INTER_CANISTER_PAYLOAD_IN_BYTES.get(), - subnet_size, - cost_schedule, + subnet_cycles_config, ) } @@ -906,8 +837,7 @@ impl CyclesAccountManager { error_counter: &IntCounter, response: &Payload, prepayment_for_response_transmission: CompoundCycles, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { let max_expected_bytes = MAX_INTER_CANISTER_PAYLOAD_IN_BYTES.get(); let transmitted_bytes = response.size_bytes().get(); @@ -924,8 +854,7 @@ impl CyclesAccountManager { } let transmission_cost = self.scale_cost( self.config.xnet_byte_transmission_fee * transmitted_bytes, - subnet_size, - cost_schedule, + subnet_cycles_config, ); prepayment_for_response_transmission - transmission_cost.min(prepayment_for_response_transmission) @@ -953,8 +882,7 @@ impl CyclesAccountManager { canister_current_memory_usage: NumBytes, canister_current_message_memory_usage: MessageMemoryUsage, canister_reserved_balance: Cycles, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, reveal_top_up: bool, ) -> Result<(), CanisterOutOfCyclesError> { let threshold = self.freeze_threshold_cycles( @@ -963,8 +891,7 @@ impl CyclesAccountManager { canister_current_memory_usage, canister_current_message_memory_usage, system_state.compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, canister_reserved_balance, ); @@ -1119,8 +1046,7 @@ impl CyclesAccountManager { memory_usage: NumBytes, message_memory_usage: MessageMemoryUsage, compute_allocation: ComputeAllocation, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, reserved_balance: Cycles, ) -> Cycles { let threshold = self.freeze_threshold_cycles( @@ -1129,8 +1055,7 @@ impl CyclesAccountManager { memory_usage, message_memory_usage, compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, reserved_balance, ); @@ -1174,15 +1099,13 @@ impl CyclesAccountManager { pub fn execution_cost( &self, num_instructions: NumInstructions, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, execution_mode: WasmExecutionMode, ) -> CompoundCycles { self.scale_cost( self.config.update_message_execution_fee + self.convert_instructions_to_cycles(num_instructions, execution_mode), - subnet_size, - cost_schedule, + subnet_cycles_config, ) } @@ -1194,14 +1117,12 @@ impl CyclesAccountManager { pub fn variable_execution_cost( &self, num_instructions: NumInstructions, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, execution_mode: WasmExecutionMode, ) -> CompoundCycles { self.scale_cost( self.convert_instructions_to_cycles(num_instructions, execution_mode), - subnet_size, - cost_schedule, + subnet_cycles_config, ) } @@ -1213,13 +1134,11 @@ impl CyclesAccountManager { pub fn management_canister_cost( &self, num_instructions: NumInstructions, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { self.scale_cost( self.convert_instructions_to_cycles(num_instructions, WasmExecutionMode::Wasm32), - subnet_size, - cost_schedule, + subnet_cycles_config, ) } @@ -1259,8 +1178,7 @@ impl CyclesAccountManager { log: &ReplicaLogger, canister: &mut CanisterState, duration_since_last_charge: Duration, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> Result<(), CanisterOutOfCyclesError> { let CyclesBurnedRate { memory, @@ -1271,8 +1189,7 @@ impl CyclesAccountManager { canister.memory_usage(), canister.message_memory_usage(), canister.compute_allocation(), - subnet_size, - cost_schedule, + subnet_cycles_config, ); self.charge_canister_for_single_resource( @@ -1301,9 +1218,9 @@ impl CyclesAccountManager { &self, request_size: NumBytes, response_size_limit: Option, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { + let subnet_size = subnet_cycles_config.subnet_size; let response_size = match response_size_limit { Some(response_size) => response_size.get(), // Defaults to maximum response size. @@ -1316,7 +1233,7 @@ impl CyclesAccountManager { + self.config.http_response_per_byte_fee * response_size) * (subnet_size as u64); - CompoundCycles::new(amount, cost_schedule) + CompoundCycles::new(amount, subnet_cycles_config.cost_schedule) } pub fn http_request_fee_v2( @@ -1326,10 +1243,9 @@ impl CyclesAccountManager { raw_response_size: NumBytes, transform: NumInstructions, transformed_response_size: NumBytes, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> CompoundCycles { - let n = subnet_size as u64; + let n = subnet_cycles_config.subnet_size as u64; let amount = (Cycles::new(1_000_000) + Cycles::new(50) * request_size.get() + Cycles::new(140_000) * n @@ -1340,15 +1256,14 @@ impl CyclesAccountManager { + (Cycles::new(10) * n + Cycles::new(650)) * transformed_response_size.get()) * n; - CompoundCycles::new(amount, cost_schedule) + CompoundCycles::new(amount, subnet_cycles_config.cost_schedule) } pub fn http_request_fee_beta( &self, request_size: NumBytes, response_size_limit: Option, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, payload_size: NumBytes, ) -> CompoundCycles { let max_response_size = match response_size_limit { @@ -1357,14 +1272,14 @@ impl CyclesAccountManager { None => MAX_CANISTER_HTTP_RESPONSE_BYTES, }; let amount = (Cycles::new(4_000_000) - + Cycles::new(50_000) * (subnet_size as u64) + + Cycles::new(50_000) * (subnet_cycles_config.subnet_size as u64) + Cycles::new(50) * request_size.get() + Cycles::new(50) * max_response_size + Cycles::new(750) * payload_size.get() - + Cycles::new(30) * (subnet_size as u64) * payload_size.get()) - * (subnet_size as u64); + + Cycles::new(30) * (subnet_cycles_config.subnet_size as u64) * payload_size.get()) + * (subnet_cycles_config.subnet_size as u64); - CompoundCycles::new(amount, cost_schedule) + CompoundCycles::new(amount, subnet_cycles_config.cost_schedule) } /// Returns the default value of the reserved balance limit for the case @@ -1376,28 +1291,25 @@ impl CyclesAccountManager { pub fn fetch_canister_logs_fee( &self, response_size: NumBytes, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> Cycles { - match cost_schedule { + match subnet_cycles_config.cost_schedule { CanisterCyclesCostSchedule::Free => Cycles::new(0), CanisterCyclesCostSchedule::Normal => { (self.config.fetch_canister_logs_base_fee + self.config.fetch_canister_logs_per_byte_fee * response_size.get()) - * subnet_size + * subnet_cycles_config.subnet_size } } } pub fn max_fetch_canister_logs_fee( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> Cycles { self.fetch_canister_logs_fee( NumBytes::new(MAX_FETCH_CANISTER_LOGS_RESPONSE_BYTES as u64), - subnet_size, - cost_schedule, + subnet_cycles_config, ) } diff --git a/rs/cycles_account_manager/src/cycles_account_manager/tests.rs b/rs/cycles_account_manager/src/cycles_account_manager/tests.rs index 9f81c3d7827d..274095c078c1 100644 --- a/rs/cycles_account_manager/src/cycles_account_manager/tests.rs +++ b/rs/cycles_account_manager/src/cycles_account_manager/tests.rs @@ -44,34 +44,52 @@ fn test_scale_cost() { let cost = Cycles::new(13_000); assert_eq!( - cam.scale_cost::(cost, 0, CanisterCyclesCostSchedule::Normal) - .real(), + cam.scale_cost::( + cost, + CyclesAccountManagerSubnetConfig::new(0, CanisterCyclesCostSchedule::Normal) + ) + .real(), Cycles::new(0) ); assert_eq!( - cam.scale_cost::(cost, 1, CanisterCyclesCostSchedule::Normal) - .real(), + cam.scale_cost::( + cost, + CyclesAccountManagerSubnetConfig::new(1, CanisterCyclesCostSchedule::Normal) + ) + .real(), Cycles::new(1_000) ); assert_eq!( - cam.scale_cost::(cost, 6, CanisterCyclesCostSchedule::Normal) - .real(), + cam.scale_cost::( + cost, + CyclesAccountManagerSubnetConfig::new(6, CanisterCyclesCostSchedule::Normal) + ) + .real(), Cycles::new(6_000) ); assert_eq!( - cam.scale_cost::(cost, 13, CanisterCyclesCostSchedule::Normal) - .real(), + cam.scale_cost::( + cost, + CyclesAccountManagerSubnetConfig::new(13, CanisterCyclesCostSchedule::Normal) + ) + .real(), Cycles::new(13_000) ); assert_eq!( - cam.scale_cost::(cost, 26, CanisterCyclesCostSchedule::Normal) - .real(), + cam.scale_cost::( + cost, + CyclesAccountManagerSubnetConfig::new(26, CanisterCyclesCostSchedule::Normal) + ) + .real(), Cycles::new(26_000) ); assert_eq!( - cam.scale_cost::(cost, 26, CanisterCyclesCostSchedule::Free) - .real(), + cam.scale_cost::( + cost, + CyclesAccountManagerSubnetConfig::new(26, CanisterCyclesCostSchedule::Free) + ) + .real(), Cycles::new(0) ); @@ -79,8 +97,7 @@ fn test_scale_cost() { assert_eq!( cam.scale_cost::( Cycles::new(u128::MAX), - 1_000_000, - CanisterCyclesCostSchedule::Normal + CyclesAccountManagerSubnetConfig::new(1_000_000, CanisterCyclesCostSchedule::Normal) ) .real(), Cycles::new(u128::MAX) / reference_subnet_size @@ -146,8 +163,10 @@ fn http_requests_fee_scale() { .http_request_fee( request_size, None, - reference_subnet_size as usize, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + reference_subnet_size as usize, + CanisterCyclesCostSchedule::Normal, + ), ) .real(), Cycles::from(1_603_786_800_u64) * reference_subnet_size @@ -159,8 +178,10 @@ fn http_requests_fee_scale() { .http_request_fee( request_size, None, - subnet_size as usize, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + subnet_size as usize, + CanisterCyclesCostSchedule::Normal, + ), ) .real(), Cycles::from(1_605_046_800_u64) * subnet_size @@ -184,8 +205,7 @@ fn test_cycles_burn() { 0.into(), MessageMemoryUsage::ZERO, ComputeAllocation::default(), - 13, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new(13, CanisterCyclesCostSchedule::Normal), Cycles::new(0) ), amount_to_burn @@ -203,8 +223,7 @@ fn test_cycles_burn() { 0.into(), MessageMemoryUsage::ZERO, ComputeAllocation::default(), - 13, - CanisterCyclesCostSchedule::Free, + CyclesAccountManagerSubnetConfig::new(13, CanisterCyclesCostSchedule::Free), Cycles::new(0) ), amount_to_burn diff --git a/rs/cycles_account_manager/src/lib.rs b/rs/cycles_account_manager/src/lib.rs index 4dfda6e477bd..f25579e6ddd5 100644 --- a/rs/cycles_account_manager/src/lib.rs +++ b/rs/cycles_account_manager/src/lib.rs @@ -23,3 +23,4 @@ mod cycles_account_manager; pub use cycles_account_manager::{ CyclesAccountManager, CyclesAccountManagerError, IngressInductionCost, ResourceSaturation, }; +pub use ic_types_cycles::CyclesAccountManagerSubnetConfig; diff --git a/rs/cycles_account_manager/tests/cycles_account_manager.rs b/rs/cycles_account_manager/tests/cycles_account_manager.rs index b6b2bf4c50f1..0c23bfe65466 100644 --- a/rs/cycles_account_manager/tests/cycles_account_manager.rs +++ b/rs/cycles_account_manager/tests/cycles_account_manager.rs @@ -1,6 +1,8 @@ use ic_base_types::NumSeconds; use ic_config::subnet_config::{CyclesAccountManagerConfig, SubnetSecurity}; -use ic_cycles_account_manager::{IngressInductionCost, ResourceSaturation}; +use ic_cycles_account_manager::{ + CyclesAccountManagerSubnetConfig, IngressInductionCost, ResourceSaturation, +}; use ic_interfaces::execution_environment::{CanisterOutOfCyclesError, MessageMemoryUsage}; use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_logger::replica_logger::no_op_logger; @@ -44,9 +46,8 @@ fn xnet_call_total_fee_free() { Cycles::new(0), cam.xnet_call_total_fee( NumBytes::new(9999), - SMALL_APP_SUBNET_MAX_SIZE, + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule), WasmExecutionMode::Wasm32, - cost_schedule ), ); } @@ -54,6 +55,8 @@ fn xnet_call_total_fee_free() { #[test] fn test_can_charge_application_subnets() { let cost_schedule = CanisterCyclesCostSchedule::Normal; + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); with_test_replica_logger(|log| { for subnet_type in &[ SubnetType::Application, @@ -65,7 +68,6 @@ fn test_can_charge_application_subnets() { MemoryAllocation::from(NumBytes::from(1 << 20)), ] { for freeze_threshold in &[NumSeconds::from(1000), NumSeconds::from(0)] { - let subnet_size = SMALL_APP_SUBNET_MAX_SIZE; let cycles_account_manager = CyclesAccountManagerBuilder::new() .with_subnet_type(*subnet_type) .build(); @@ -89,18 +91,13 @@ fn test_can_charge_application_subnets() { let memory = memory_allocation.allocated_bytes(canister.memory_usage()); let expected_fee = cycles_account_manager - .compute_allocation_cost( - compute_allocation, - duration, - subnet_size, - cost_schedule, - ) + .compute_allocation_cost(compute_allocation, duration, subnet_cycles_config) .real() + cycles_account_manager - .memory_cost(memory, duration, subnet_size, cost_schedule) + .memory_cost(memory, duration, subnet_cycles_config) .real() + cycles_account_manager - .canister_base_cost(memory, duration, subnet_size, cost_schedule) + .canister_base_cost(memory, duration, subnet_cycles_config) .real(); let initial_cycles = expected_fee; canister.system_state.add_cycles(initial_cycles); @@ -110,8 +107,7 @@ fn test_can_charge_application_subnets() { &log, &mut canister, duration, - subnet_size, - cost_schedule, + subnet_cycles_config, ) .unwrap(); assert_eq!(canister.system_state.balance(), Cycles::zero()); @@ -131,6 +127,8 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { best_effort: NumBytes::new(2 << 20), }; let amount = Cycles::new(200); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); { let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); let mut system_state = SystemState::new_running_for_testing( @@ -150,8 +148,7 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { ComputeAllocation::default(), &mut new_balance, amount, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), false, ), @@ -164,8 +161,7 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { NumBytes::from(0), MessageMemoryUsage::ZERO, ComputeAllocation::default(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), ); assert_eq!(system_state.balance(), initial_cycles - threshold - amount); @@ -190,8 +186,7 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { ComputeAllocation::default(), &mut new_balance, amount, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), false, ), @@ -204,8 +199,7 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { NumBytes::from(0), MessageMemoryUsage::ZERO, ComputeAllocation::default(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), ); assert_eq!(system_state.balance(), initial_cycles - threshold - amount); @@ -232,8 +226,7 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { ComputeAllocation::default(), &mut new_balance, amount, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), false, ), @@ -246,8 +239,7 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { memory_usage, message_memory_usage, ComputeAllocation::default(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), ); assert_eq!(system_state.balance(), initial_cycles - threshold - amount); @@ -272,8 +264,7 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { ComputeAllocation::default(), &mut balance, amount, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), false, ), @@ -287,8 +278,7 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { memory_usage, message_memory_usage, ComputeAllocation::default(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), ), reveal_top_up: false, @@ -301,6 +291,7 @@ fn withdraw_cycles_with_not_enough_balance_returns_error() { fn verify_no_cycles_charged_for_message_execution_on_system_subnets() { let cost_schedule = CanisterCyclesCostSchedule::Normal; let subnet_size = SMALL_APP_SUBNET_MAX_SIZE; + let subnet_cycles_config = CyclesAccountManagerSubnetConfig::new(subnet_size, cost_schedule); let mut system_state = SystemStateBuilder::new().build(); let cycles_account_manager = CyclesAccountManagerBuilder::new() .with_subnet_type(SubnetType::System) @@ -314,8 +305,7 @@ fn verify_no_cycles_charged_for_message_execution_on_system_subnets() { MessageMemoryUsage::ZERO, ComputeAllocation::default(), NumInstructions::from(1_000_000), - subnet_size, - cost_schedule, + subnet_cycles_config, false, WASM_EXECUTION_MODE, ) @@ -329,8 +319,7 @@ fn verify_no_cycles_charged_for_message_execution_on_system_subnets() { NumInstructions::from(1_000_000), cycles, &no_op_counter, - subnet_size, - cost_schedule, + subnet_cycles_config, WASM_EXECUTION_MODE, &no_op_logger(), ); @@ -341,6 +330,7 @@ fn verify_no_cycles_charged_for_message_execution_on_system_subnets() { fn verify_no_cycles_charged_for_message_execution_on_free_schedule() { let cost_schedule = CanisterCyclesCostSchedule::Free; let subnet_size = SMALL_APP_SUBNET_MAX_SIZE; + let subnet_cycles_config = CyclesAccountManagerSubnetConfig::new(subnet_size, cost_schedule); let mut system_state = SystemStateBuilder::new().build(); let cycles_account_manager = CyclesAccountManagerBuilder::new() .with_subnet_type(SubnetType::Application) @@ -354,8 +344,7 @@ fn verify_no_cycles_charged_for_message_execution_on_free_schedule() { MessageMemoryUsage::ZERO, ComputeAllocation::default(), NumInstructions::from(1_000_000), - subnet_size, - cost_schedule, + subnet_cycles_config, false, WASM_EXECUTION_MODE, ) @@ -369,8 +358,7 @@ fn verify_no_cycles_charged_for_message_execution_on_free_schedule() { NumInstructions::from(1_000_000), cycles, &no_op_counter, - subnet_size, - cost_schedule, + subnet_cycles_config, WASM_EXECUTION_MODE, &no_op_logger(), ); @@ -393,12 +381,13 @@ fn ingress_induction_cost_valid_subnet_message() { let effective_canister_id = extract_effective_canister_id(signed_ingress_content).unwrap(); let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); let num_bytes = msg.binary().len(); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); let cost = cycles_account_manager .ingress_induction_cost_from_bytes( NumBytes::from(num_bytes as u64), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real(); if let CanisterCyclesCostSchedule::Free = cost_schedule { @@ -408,8 +397,7 @@ fn ingress_induction_cost_valid_subnet_message() { cycles_account_manager.ingress_induction_cost( &msg, effective_canister_id, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ), IngressInductionCost::Fee { payer: canister_test_id(0), @@ -424,6 +412,8 @@ fn charging_removes_canisters_with_insufficient_balance() { let cost_schedule = CanisterCyclesCostSchedule::Normal; with_test_replica_logger(|log| { let subnet_size = SMALL_APP_SUBNET_MAX_SIZE; + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(subnet_size, cost_schedule); let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); let mut canister = new_canister_state( @@ -439,8 +429,7 @@ fn charging_removes_canisters_with_insufficient_balance() { &log, &mut canister, Duration::from_secs(1), - subnet_size, - cost_schedule, + subnet_cycles_config, ) .unwrap(); @@ -457,8 +446,7 @@ fn charging_removes_canisters_with_insufficient_balance() { &log, &mut canister, Duration::from_secs(1), - subnet_size, - cost_schedule, + subnet_cycles_config, ) .unwrap_err(); @@ -475,8 +463,7 @@ fn charging_removes_canisters_with_insufficient_balance() { &log, &mut canister, Duration::from_secs(1), - subnet_size, - cost_schedule, + subnet_cycles_config, ) .unwrap_err(); }) @@ -490,6 +477,8 @@ fn charge_canister_for_memory_usage() { const MEMORY_ALLOCATION: NumBytes = NumBytes::new(1 << 30); const HOUR: Duration = Duration::from_secs(3600); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); let canister_id = canister_test_id(1); @@ -524,8 +513,7 @@ fn charge_canister_for_memory_usage() { &log, &mut canister, HOUR, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .unwrap(); @@ -533,15 +521,10 @@ fn charge_canister_for_memory_usage() { let cycles_burned = INITIAL_BALANCE - canister.system_state.balance(); assert_eq!( cycles_account_manager - .memory_cost(memory_usage, HOUR, SMALL_APP_SUBNET_MAX_SIZE, cost_schedule) + .memory_cost(memory_usage, HOUR, subnet_cycles_config,) .real() + cycles_account_manager - .canister_base_cost( - memory_usage, - HOUR, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule - ) + .canister_base_cost(memory_usage, HOUR, subnet_cycles_config) .real(), cycles_burned ) @@ -556,6 +539,8 @@ fn do_not_charge_canister_for_memory_usage_free_schedule() { const MEMORY_ALLOCATION: NumBytes = NumBytes::new(1 << 30); const HOUR: Duration = Duration::from_secs(3600); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); let canister_id = canister_test_id(1); @@ -590,8 +575,7 @@ fn do_not_charge_canister_for_memory_usage_free_schedule() { &log, &mut canister, HOUR, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .unwrap(); @@ -600,7 +584,7 @@ fn do_not_charge_canister_for_memory_usage_free_schedule() { assert_eq!(cycles_burned, Cycles::new(0)); assert_eq!( cycles_account_manager - .memory_cost(memory_usage, HOUR, SMALL_APP_SUBNET_MAX_SIZE, cost_schedule) + .memory_cost(memory_usage, HOUR, subnet_cycles_config,) .real(), cycles_burned ) @@ -613,6 +597,8 @@ fn do_not_charge_canister_for_compute_allocation_free_schedule() { with_test_replica_logger(|log| { const HOUR: Duration = Duration::from_secs(3600); let compute_allocation = ComputeAllocation::try_from(20).unwrap(); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); @@ -644,18 +630,12 @@ fn do_not_charge_canister_for_compute_allocation_free_schedule() { &log, &mut canister, HOUR, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .unwrap(); let expected_fee = cycles_account_manager - .compute_allocation_cost( - compute_allocation, - HOUR, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, - ) + .compute_allocation_cost(compute_allocation, HOUR, subnet_cycles_config) .real(); assert_eq!(expected_fee, Cycles::zero()); @@ -822,6 +802,8 @@ fn test_consume_with_threshold() { fn cycles_withdraw_for_execution() { let cost_schedule = CanisterCyclesCostSchedule::Normal; let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); let memory_usage = NumBytes::from(4 << 30); let message_memory_usage = MessageMemoryUsage { guaranteed_response: NumBytes::new(6 << 20), @@ -846,8 +828,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, system_state.compute_allocation, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), ); @@ -860,8 +841,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, amount, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_ok() @@ -874,8 +854,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, amount, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_err() @@ -893,8 +872,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, system_state.reserved_balance(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_ok() @@ -906,8 +884,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, compound_exec_cycles_max, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_ok() @@ -920,8 +897,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, system_state.reserved_balance(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ), Err(CanisterOutOfCyclesError { @@ -941,8 +917,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, compound_exec_cycles_max, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_err() @@ -954,8 +929,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, CompoundCycles::::new(Cycles::new(10), cost_schedule), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_err() @@ -967,8 +941,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, CompoundCycles::::new(Cycles::new(1), cost_schedule), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_err() @@ -980,8 +953,7 @@ fn cycles_withdraw_for_execution() { memory_usage, message_memory_usage, CompoundCycles::::new(Cycles::zero(), cost_schedule), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_ok() @@ -993,6 +965,8 @@ fn cycles_withdraw_for_execution() { fn do_not_withdraw_cycles_for_execution_free_schedule() { let cost_schedule = CanisterCyclesCostSchedule::Free; let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); let memory_usage = NumBytes::from(4 << 30); let message_memory_usage = MessageMemoryUsage { guaranteed_response: NumBytes::new(6 << 20), @@ -1017,8 +991,7 @@ fn do_not_withdraw_cycles_for_execution_free_schedule() { memory_usage, message_memory_usage, system_state.compute_allocation, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, system_state.reserved_balance(), ); @@ -1031,8 +1004,7 @@ fn do_not_withdraw_cycles_for_execution_free_schedule() { memory_usage, message_memory_usage, amount, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_ok() @@ -1049,8 +1021,7 @@ fn do_not_withdraw_cycles_for_execution_free_schedule() { memory_usage, message_memory_usage, system_state.reserved_balance(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, false, ) .is_ok() @@ -1073,8 +1044,7 @@ fn withdraw_execution_cycles_consumes_cycles() { MessageMemoryUsage::ZERO, ComputeAllocation::default(), NumInstructions::from(1_000_000), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule), false, WASM_EXECUTION_MODE, ) @@ -1102,8 +1072,7 @@ fn withdraw_for_transfer_does_not_consume_cycles() { ComputeAllocation::default(), &mut balance, Cycles::new(1_000_000), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule), system_state.reserved_balance(), false, ) @@ -1129,8 +1098,7 @@ fn consume_cycles_updates_consumed_cycles() { NumBytes::from(0), MessageMemoryUsage::ZERO, CompoundCycles::::new(Cycles::new(1_000_000), cost_schedule), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule), false, ) .unwrap(); @@ -1260,8 +1228,7 @@ fn withdraw_cycles_for_transfer_checks_reserved_balance() { ComputeAllocation::default(), &mut new_balance, Cycles::new(1_000_000), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule), system_state.reserved_balance(), false, ) @@ -1273,6 +1240,8 @@ fn withdraw_cycles_for_transfer_checks_reserved_balance() { fn freezing_threshold_uses_reserved_balance() { let cost_schedule = CanisterCyclesCostSchedule::Normal; let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); let threshold_without_reserved = cycles_account_manager.freeze_threshold_cycles( NumSeconds::from(1_000), MemoryAllocation::default(), @@ -1282,8 +1251,7 @@ fn freezing_threshold_uses_reserved_balance() { best_effort: NumBytes::new(0), }, ComputeAllocation::default(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, Cycles::new(0), ); @@ -1296,8 +1264,7 @@ fn freezing_threshold_uses_reserved_balance() { best_effort: NumBytes::new(0), }, ComputeAllocation::default(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, Cycles::new(1_000), ); @@ -1350,6 +1317,8 @@ fn scaling_of_resource_saturation() { #[test] fn test_storage_reservation_cycles() { let cost_schedule = CanisterCyclesCostSchedule::Normal; + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); const GB: u64 = 1024 * 1024 * 1024; let cfg = CyclesAccountManagerConfig::application_subnet(SubnetSecurity::None); @@ -1361,8 +1330,7 @@ fn test_storage_reservation_cycles() { cam.storage_reservation_cycles( NumBytes::new(100 * GB), &ResourceSaturation::new(0, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); @@ -1383,8 +1351,7 @@ fn test_storage_reservation_cycles() { cam.storage_reservation_cycles( NumBytes::new(101 * GB), &ResourceSaturation::new(0, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); @@ -1407,8 +1374,7 @@ fn test_storage_reservation_cycles() { cam.storage_reservation_cycles( NumBytes::new(40 * GB), &ResourceSaturation::new(90 * GB, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); @@ -1431,8 +1397,7 @@ fn test_storage_reservation_cycles() { cam.storage_reservation_cycles( NumBytes::new(40 * GB), &ResourceSaturation::new(100 * GB, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); @@ -1453,26 +1418,26 @@ fn test_storage_reservation_cycles() { cam.storage_reservation_cycles( NumBytes::new(40 * GB), &ResourceSaturation::new(160 * GB, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); // The total reserved cycles of small allocations should match that of one // large allocation. + let thirteen_node_config = CyclesAccountManagerSubnetConfig::new(13, cost_schedule); let rs0 = ResourceSaturation::new(0, 100 * GB, 1000 * GB); let mut total = Cycles::zero(); let mut rs = rs0.clone(); for _ in 0..1000 { total += cam - .storage_reservation_cycles(NumBytes::new(GB), &rs, 13, cost_schedule) + .storage_reservation_cycles(NumBytes::new(GB), &rs, thirteen_node_config) .real(); rs = rs.add(GB); } assert_eq!( total, - cam.storage_reservation_cycles(NumBytes::new(1000 * GB), &rs0, 13, cost_schedule) + cam.storage_reservation_cycles(NumBytes::new(1000 * GB), &rs0, thirteen_node_config) .real() ) } @@ -1480,6 +1445,8 @@ fn test_storage_reservation_cycles() { #[test] fn test_storage_reservation_cycles_free() { let cost_schedule = CanisterCyclesCostSchedule::Free; + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); const GB: u64 = 1024 * 1024 * 1024; let cam = CyclesAccountManagerBuilder::new().build(); @@ -1490,8 +1457,7 @@ fn test_storage_reservation_cycles_free() { cam.storage_reservation_cycles( NumBytes::new(100 * GB), &ResourceSaturation::new(0, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); @@ -1502,8 +1468,7 @@ fn test_storage_reservation_cycles_free() { cam.storage_reservation_cycles( NumBytes::new(101 * GB), &ResourceSaturation::new(0, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); @@ -1514,8 +1479,7 @@ fn test_storage_reservation_cycles_free() { cam.storage_reservation_cycles( NumBytes::new(40 * GB), &ResourceSaturation::new(90 * GB, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); @@ -1526,8 +1490,7 @@ fn test_storage_reservation_cycles_free() { cam.storage_reservation_cycles( NumBytes::new(40 * GB), &ResourceSaturation::new(100 * GB, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); @@ -1538,8 +1501,7 @@ fn test_storage_reservation_cycles_free() { cam.storage_reservation_cycles( NumBytes::new(40 * GB), &ResourceSaturation::new(160 * GB, 100 * GB, 200 * GB), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real() ); @@ -1549,6 +1511,7 @@ fn test_storage_reservation_cycles_free() { fn variable_execution_cost_matches_refund() { let cost_schedule = CanisterCyclesCostSchedule::Normal; let subnet_size = SMALL_APP_SUBNET_MAX_SIZE; + let subnet_cycles_config = CyclesAccountManagerSubnetConfig::new(subnet_size, cost_schedule); let cam = CyclesAccountManagerBuilder::new() .with_subnet_type(SubnetType::Application) .build(); @@ -1564,8 +1527,7 @@ fn variable_execution_cost_matches_refund() { MessageMemoryUsage::ZERO, ComputeAllocation::default(), n_max, - subnet_size, - cost_schedule, + subnet_cycles_config, false, WASM_EXECUTION_MODE, ) @@ -1580,14 +1542,13 @@ fn variable_execution_cost_matches_refund() { n_max, prepaid, &no_op_counter, - subnet_size, - cost_schedule, + subnet_cycles_config, WASM_EXECUTION_MODE, &no_op_logger(), ); let expected_refund = cam - .variable_execution_cost(n_refund, subnet_size, cost_schedule, WASM_EXECUTION_MODE) + .variable_execution_cost(n_refund, subnet_cycles_config, WASM_EXECUTION_MODE) .real(); assert_eq!( system_state.balance(), diff --git a/rs/embedders/BUILD.bazel b/rs/embedders/BUILD.bazel index 65baf336cb47..bdb446fa26e1 100644 --- a/rs/embedders/BUILD.bazel +++ b/rs/embedders/BUILD.bazel @@ -185,6 +185,7 @@ rust_ic_test_suite_with_extra_srcs( "//rs/config", "//rs/cycles_account_manager", "//rs/interfaces", + "//rs/limits", "//rs/monitoring/logger", "//rs/nns/constants", "//rs/registry/routing_table", diff --git a/rs/embedders/fuzz/BUILD.bazel b/rs/embedders/fuzz/BUILD.bazel index 3be081dad3d2..30ba66efc92e 100644 --- a/rs/embedders/fuzz/BUILD.bazel +++ b/rs/embedders/fuzz/BUILD.bazel @@ -25,6 +25,7 @@ rust_library( "//rs/cycles_account_manager", "//rs/embedders", "//rs/interfaces", + "//rs/limits", "//rs/monitoring/logger", "//rs/monitoring/metrics", "//rs/registry/subnet_type", diff --git a/rs/embedders/fuzz/Cargo.toml b/rs/embedders/fuzz/Cargo.toml index 06dea7d02957..1712440e8f0c 100644 --- a/rs/embedders/fuzz/Cargo.toml +++ b/rs/embedders/fuzz/Cargo.toml @@ -17,6 +17,7 @@ ic-config = { path = "../../config" } ic-cycles-account-manager = { path = "../../cycles_account_manager" } ic-embedders = { path = "../../embedders" } ic-interfaces = { path = "../../interfaces" } +ic-limits = { path = "../../limits" } ic-logger = { path = "../../monitoring/logger" } ic-management-canister-types-private = { path = "../../types/management_canister_types" } ic-metrics = { path = "../../monitoring/metrics" } diff --git a/rs/embedders/fuzz/src/wasm_executor.rs b/rs/embedders/fuzz/src/wasm_executor.rs index c8fa0dd0fc70..8ccba479f2b6 100644 --- a/rs/embedders/fuzz/src/wasm_executor.rs +++ b/rs/embedders/fuzz/src/wasm_executor.rs @@ -3,7 +3,7 @@ use ic_config::{ embedders::Config as EmbeddersConfig, execution_environment::Config as HypervisorConfig, subnet_config::SchedulerConfig, }; -use ic_cycles_account_manager::ResourceSaturation; +use ic_cycles_account_manager::{CyclesAccountManagerSubnetConfig, ResourceSaturation}; use ic_embedders::{ CompilationCache, CompilationCacheBuilder, WasmExecutionInput, WasmtimeEmbedder, wasm_executor::{WasmExecutionResult, WasmExecutor, WasmExecutorImpl}, @@ -15,6 +15,7 @@ use ic_embedders::{ use ic_interfaces::execution_environment::{ ExecutionMode, MessageMemoryUsage, SubnetAvailableMemory, }; +use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_logger::replica_logger::no_op_logger; use ic_metrics::MetricsRegistry; use ic_registry_subnet_type::SubnetType; @@ -164,7 +165,10 @@ pub(crate) fn get_sandbox_safe_system_state( Default::default(), api_type.caller(), api_type.call_context_id(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ) } diff --git a/rs/embedders/src/wasmtime_embedder/system_api.rs b/rs/embedders/src/wasmtime_embedder/system_api.rs index bbd2126fde1c..bc6cf91d3084 100644 --- a/rs/embedders/src/wasmtime_embedder/system_api.rs +++ b/rs/embedders/src/wasmtime_embedder/system_api.rs @@ -1284,7 +1284,7 @@ impl SystemApiImpl { } pub fn get_cost_schedule(&self) -> CanisterCyclesCostSchedule { - self.sandbox_safe_system_state.cost_schedule + self.sandbox_safe_system_state.cost_schedule() } /// Note that this function is made public only for the tests @@ -4296,7 +4296,7 @@ impl SystemApi for SystemApiImpl { dst: usize, heap: &mut [u8], ) -> HypervisorResult<()> { - let subnet_size = self.sandbox_safe_system_state.subnet_size; + let subnet_cycles_config = self.sandbox_safe_system_state.subnet_cycles_config; let execution_mode = WasmExecutionMode::from_is_wasm64(self.sandbox_safe_system_state.is_wasm64_execution); let cost = self @@ -4304,9 +4304,8 @@ impl SystemApi for SystemApiImpl { .get_cycles_account_manager() .xnet_call_total_fee( (method_name_size.saturating_add(payload_size)).into(), - subnet_size, + subnet_cycles_config, execution_mode, - self.get_cost_schedule(), ); copy_cycles_to_heap(cost, dst, heap, "ic0_cost_call")?; trace_syscall!(self, CostCall, cost); @@ -4314,11 +4313,11 @@ impl SystemApi for SystemApiImpl { } fn ic0_cost_create_canister(&self, dst: usize, heap: &mut [u8]) -> HypervisorResult<()> { - let subnet_size = self.sandbox_safe_system_state.subnet_size; + let subnet_cycles_config = self.sandbox_safe_system_state.subnet_cycles_config; let cost = self .sandbox_safe_system_state .get_cycles_account_manager() - .canister_creation_fee(subnet_size, self.get_cost_schedule()); + .canister_creation_fee(subnet_cycles_config); copy_cycles_to_heap(cost.real(), dst, heap, "ic0_cost_create_canister")?; trace_syscall!(self, CostCreateCanister, cost); Ok(()) @@ -4331,15 +4330,14 @@ impl SystemApi for SystemApiImpl { dst: usize, heap: &mut [u8], ) -> HypervisorResult<()> { - let subnet_size = self.sandbox_safe_system_state.subnet_size; + let subnet_cycles_config = self.sandbox_safe_system_state.subnet_cycles_config; let cost = self .sandbox_safe_system_state .get_cycles_account_manager() .http_request_fee( request_size.into(), Some(max_res_bytes.into()), - subnet_size, - self.get_cost_schedule(), + subnet_cycles_config, ); copy_cycles_to_heap(cost.real(), dst, heap, "ic0_cost_http_request")?; trace_syscall!(self, CostHttpRequest, cost); @@ -4381,7 +4379,7 @@ impl SystemApi for SystemApiImpl { } })?; - let subnet_size = self.sandbox_safe_system_state.subnet_size; + let subnet_cycles_config = self.sandbox_safe_system_state.subnet_cycles_config; let cost = self .sandbox_safe_system_state .get_cycles_account_manager() @@ -4391,8 +4389,7 @@ impl SystemApi for SystemApiImpl { cost_params_v2.raw_response_bytes.into(), cost_params_v2.transform_instructions.into(), cost_params_v2.transformed_response_bytes.into(), - subnet_size, - self.get_cost_schedule(), + subnet_cycles_config, ); copy_cycles_to_heap(cost.real(), dst, heap, "ic0_cost_http_request_v2")?; trace_syscall!(self, CostHttpRequestV2, cost); @@ -4425,15 +4422,14 @@ impl SystemApi for SystemApiImpl { return Ok(CostReturnCode::UnknownCurveOrAlgorithm as u32); }; let key = MasterPublicKeyId::Ecdsa(EcdsaKeyId { curve, name }); - let Some((subnet_size, cost_schedule, _)) = - self.sandbox_safe_system_state.get_key_subnet_details(key) + let Some(subnet_cycles_config) = self.sandbox_safe_system_state.get_key_subnet_details(key) else { return Ok(CostReturnCode::UnknownKey as u32); }; let cost = self .sandbox_safe_system_state .get_cycles_account_manager() - .ecdsa_signature_fee(subnet_size, cost_schedule); + .ecdsa_signature_fee(subnet_cycles_config); copy_cycles_to_heap(cost.real(), dst, heap, "ic0_cost_sign_with_ecdsa")?; trace_syscall!(self, CostSignWithEcdsa, cost); Ok(CostReturnCode::Success as u32) @@ -4465,15 +4461,14 @@ impl SystemApi for SystemApiImpl { return Ok(CostReturnCode::UnknownCurveOrAlgorithm as u32); }; let key = MasterPublicKeyId::Schnorr(SchnorrKeyId { algorithm, name }); - let Some((subnet_size, cost_schedule, _)) = - self.sandbox_safe_system_state.get_key_subnet_details(key) + let Some(subnet_cycles_config) = self.sandbox_safe_system_state.get_key_subnet_details(key) else { return Ok(CostReturnCode::UnknownKey as u32); }; let cost = self .sandbox_safe_system_state .get_cycles_account_manager() - .schnorr_signature_fee(subnet_size, cost_schedule); + .schnorr_signature_fee(subnet_cycles_config); copy_cycles_to_heap(cost.real(), dst, heap, "ic0_cost_sign_with_schnorr")?; trace_syscall!(self, CostSignWithSchnorr, cost); Ok(CostReturnCode::Success as u32) @@ -4505,15 +4500,14 @@ impl SystemApi for SystemApiImpl { return Ok(CostReturnCode::UnknownCurveOrAlgorithm as u32); }; let key = MasterPublicKeyId::VetKd(VetKdKeyId { curve, name }); - let Some((subnet_size, cost_schedule, _)) = - self.sandbox_safe_system_state.get_key_subnet_details(key) + let Some(subnet_cycles_config) = self.sandbox_safe_system_state.get_key_subnet_details(key) else { return Ok(CostReturnCode::UnknownKey as u32); }; let cost = self .sandbox_safe_system_state .get_cycles_account_manager() - .vetkd_fee(subnet_size, cost_schedule); + .vetkd_fee(subnet_cycles_config); copy_cycles_to_heap(cost.real(), dst, heap, "ic0_cost_vetkd_derive_key")?; trace_syscall!(self, CostVetkdDeriveEncryptedKey, cost); Ok(CostReturnCode::Success as u32) diff --git a/rs/embedders/src/wasmtime_embedder/system_api/sandbox_safe_system_state.rs b/rs/embedders/src/wasmtime_embedder/system_api/sandbox_safe_system_state.rs index 949f747e6cf1..77f020ba0a16 100644 --- a/rs/embedders/src/wasmtime_embedder/system_api/sandbox_safe_system_state.rs +++ b/rs/embedders/src/wasmtime_embedder/system_api/sandbox_safe_system_state.rs @@ -10,7 +10,7 @@ use ic_cycles_account_manager::{ }; use ic_error_types::{ErrorCode, RejectCode, UserError}; use ic_interfaces::execution_environment::{HypervisorError, HypervisorResult, MessageMemoryUsage}; -use ic_limits::{LOG_CANISTER_OPERATION_CYCLES_THRESHOLD, SMALL_APP_SUBNET_MAX_SIZE}; +use ic_limits::LOG_CANISTER_OPERATION_CYCLES_THRESHOLD; use ic_logger::{ReplicaLogger, info}; use ic_management_canister_types_private::{ CanisterStatusType, CreateCanisterArgs, IC_00, InstallChunkedCodeArgs, InstallCodeArgsV2, @@ -31,8 +31,9 @@ use ic_types::{ time::CoarseTime, }; use ic_types_cycles::{ - BurnedCycles, CanisterCyclesCostSchedule, CompoundCycles, Cycles, CyclesUseCaseKind, - Instructions, RequestAndResponseTransmission, + BurnedCycles, CanisterCyclesCostSchedule, CompoundCycles, Cycles, + CyclesAccountManagerSubnetConfig, CyclesUseCaseKind, Instructions, + RequestAndResponseTransmission, }; use ic_wasm_types::WasmEngineError; use serde::{Deserialize, Serialize}; @@ -631,8 +632,7 @@ pub struct SandboxSafeSystemState { pub(super) canister_id: CanisterId, pub(super) status: CanisterStatusView, pub(super) subnet_type: SubnetType, - pub(super) subnet_size: usize, - pub(super) cost_schedule: CanisterCyclesCostSchedule, + pub(super) subnet_cycles_config: CyclesAccountManagerSubnetConfig, dirty_page_overhead: NumInstructions, freeze_threshold: NumSeconds, memory_allocation: MemoryAllocation, @@ -683,8 +683,7 @@ impl SandboxSafeSystemState { available_request_slots: BTreeMap, ic00_available_request_slots: usize, ic00_aliases: BTreeSet, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, dirty_page_overhead: NumInstructions, global_timer: CanisterTimer, canister_version: u64, @@ -700,8 +699,7 @@ impl SandboxSafeSystemState { canister_id, status, subnet_type: cycles_account_manager.subnet_type(), - subnet_size, - cost_schedule, + subnet_cycles_config, dirty_page_overhead, freeze_threshold, memory_allocation, @@ -748,7 +746,7 @@ impl SandboxSafeSystemState { request_metadata: RequestMetadata, caller: Option, call_context_id: Option, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> Self { Self::new( system_state, @@ -762,7 +760,7 @@ impl SandboxSafeSystemState { call_context_id, // We can assume a Wasm32 environment in tests for now. false, - cost_schedule, + subnet_cycles_config, ) } @@ -777,7 +775,7 @@ impl SandboxSafeSystemState { caller: Option, call_context_id: Option, is_wasm64_execution: bool, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> Self { let call_context = call_context_id.and_then(|call_context_id| { system_state @@ -818,10 +816,6 @@ impl SandboxSafeSystemState { }) .min() .unwrap_or(DEFAULT_QUEUE_CAPACITY); - let subnet_size = network_topology - .get_subnet_size(&cycles_account_manager.get_subnet_id()) - .unwrap_or(SMALL_APP_SUBNET_MAX_SIZE); - let (next_canister_log_record_idx, canister_log_memory_limit) = if system_state.log_memory_store.is_migrated() { let lms = &system_state.log_memory_store; @@ -850,8 +844,7 @@ impl SandboxSafeSystemState { available_request_slots, ic00_available_request_slots, ic00_aliases, - subnet_size, - cost_schedule, + subnet_cycles_config, dirty_page_overhead, system_state.global_timer, system_state.canister_version(), @@ -882,7 +875,7 @@ impl SandboxSafeSystemState { } pub fn cost_schedule(&self) -> CanisterCyclesCostSchedule { - self.cost_schedule + self.subnet_cycles_config.cost_schedule } pub fn set_global_timer(&mut self, timer: CanisterTimer) { @@ -916,8 +909,7 @@ impl SandboxSafeSystemState { current_memory_usage, current_message_memory_usage, self.compute_allocation, - self.subnet_size, - self.cost_schedule, + self.subnet_cycles_config, self.reserved_balance(), ); // Here we rely on the saturating subtraction for Cycles. @@ -1000,14 +992,16 @@ impl SandboxSafeSystemState { canister_current_memory_usage, canister_current_message_memory_usage, self.compute_allocation, - self.subnet_size, - self.cost_schedule, + self.subnet_cycles_config, self.reserved_balance(), ); self.update_balance_change_consuming( new_balance, ConsumedCyclesDuringExecution { - burned: Some(CompoundCycles::new(burned_cycles, self.cost_schedule)), + burned: Some(CompoundCycles::new( + burned_cycles, + self.subnet_cycles_config.cost_schedule, + )), ..Default::default() }, ); @@ -1064,8 +1058,7 @@ impl SandboxSafeSystemState { pub fn prepayment_for_response_execution(&self) -> CompoundCycles { self.cycles_account_manager .prepayment_for_response_execution( - self.subnet_size, - self.cost_schedule, + self.subnet_cycles_config, WasmExecutionMode::from_is_wasm64(self.is_wasm64_execution), ) } @@ -1074,18 +1067,15 @@ impl SandboxSafeSystemState { &self, ) -> CompoundCycles { self.cycles_account_manager - .prepayment_for_response_transmission(self.subnet_size, self.cost_schedule) + .prepayment_for_response_transmission(self.subnet_cycles_config) } pub fn xnet_total_transmission_fee( &self, payload_size: NumBytes, ) -> CompoundCycles { - self.cycles_account_manager.xnet_total_transmission_fee( - payload_size, - self.subnet_size, - self.cost_schedule, - ) + self.cycles_account_manager + .xnet_total_transmission_fee(payload_size, self.subnet_cycles_config) } pub(super) fn withdraw_cycles_for_transfer( @@ -1107,8 +1097,7 @@ impl SandboxSafeSystemState { self.compute_allocation, &mut new_balance, amount, - self.subnet_size, - self.cost_schedule, + self.subnet_cycles_config, self.reserved_balance(), reveal_top_up, ) @@ -1142,8 +1131,7 @@ impl SandboxSafeSystemState { self.compute_allocation, msg.prepayment_for_response_execution, msg.prepayment_for_call_transmission, - self.subnet_size, - self.cost_schedule, + self.subnet_cycles_config, self.reserved_balance(), // if the canister is frozen, the controller should call canister_status // to learn the top up balance instead of getting it from an error @@ -1235,8 +1223,7 @@ impl SandboxSafeSystemState { new_memory_usage, current_message_memory_usage, self.compute_allocation, - self.subnet_size, - self.cost_schedule, + self.subnet_cycles_config, self.reserved_balance(), ); if self.cycles_balance() >= threshold { @@ -1279,8 +1266,7 @@ impl SandboxSafeSystemState { current_memory_usage, new_message_memory_usage, self.compute_allocation, - self.subnet_size, - self.cost_schedule, + self.subnet_cycles_config, self.reserved_balance(), ); if self.cycles_balance() >= threshold { @@ -1350,8 +1336,7 @@ impl SandboxSafeSystemState { .storage_reservation_cycles( allocated_bytes, subnet_memory_saturation, - self.subnet_size, - self.cost_schedule, + self.subnet_cycles_config, ) .real(); @@ -1423,12 +1408,12 @@ impl SandboxSafeSystemState { } /// Look up key in `chain_key_enabled_subnets`, then extract all subnets - /// for that key and return the replication factor, cost_schedule and subnet_id of the biggest one. + /// for that key and return the subnet cycles config of the biggest one. /// These data are all returned together because their existence is contingent on the key. pub fn get_key_subnet_details( &self, key: MasterPublicKeyId, - ) -> Option<(usize, CanisterCyclesCostSchedule, SubnetId)> { + ) -> Option { let subnets_with_key = self.network_topology.chain_key_enabled_subnets(&key); subnets_with_key .iter() @@ -1439,9 +1424,10 @@ impl SandboxSafeSystemState { .subnets() .get(subnet_id)? .cost_schedule; - Some((size, cost_schedule, *subnet_id)) + Some((size, cost_schedule)) }) .max() + .map(|(size, cost_schedule)| CyclesAccountManagerSubnetConfig::new(size, cost_schedule)) } } @@ -1462,7 +1448,7 @@ mod tests { use ic_base_types::NumSeconds; use ic_config::subnet_config::{CyclesAccountManagerConfig, SchedulerConfig, SubnetSecurity}; - use ic_cycles_account_manager::CyclesAccountManager; + use ic_cycles_account_manager::{CyclesAccountManager, CyclesAccountManagerSubnetConfig}; use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_registry_subnet_type::SubnetType; use ic_replicated_state::{NetworkTopology, SystemState}; @@ -1596,8 +1582,10 @@ mod tests { BTreeMap::new(), 0, BTreeSet::new(), - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), SchedulerConfig::application_subnet().dirty_page_overhead, CanisterTimer::Inactive, 0, diff --git a/rs/embedders/src/wasmtime_embedder/tests.rs b/rs/embedders/src/wasmtime_embedder/tests.rs index 40bf5b8b15ee..14d8b7b906f7 100644 --- a/rs/embedders/src/wasmtime_embedder/tests.rs +++ b/rs/embedders/src/wasmtime_embedder/tests.rs @@ -21,10 +21,11 @@ use ic_config::{ execution_environment::{Config as HypervisorConfig, LOG_MEMORY_STORE_FEATURE}, subnet_config::SchedulerConfig, }; -use ic_cycles_account_manager::ResourceSaturation; +use ic_cycles_account_manager::{CyclesAccountManagerSubnetConfig, ResourceSaturation}; use ic_interfaces::execution_environment::{ ExecutionMode, MessageMemoryUsage, SubnetAvailableMemory, }; +use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_logger::replica_logger::no_op_logger; use ic_registry_subnet_type::SubnetType; use ic_replicated_state::page_map::TestPageAllocatorFileDescriptorImpl; @@ -78,7 +79,10 @@ fn test_wasmtime_system_api() { Default::default(), api_type.caller(), api_type.call_context_id(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); let canister_current_memory_usage = NumBytes::from(0); let canister_current_message_memory_usage = MessageMemoryUsage::ZERO; diff --git a/rs/embedders/tests/common/mod.rs b/rs/embedders/tests/common/mod.rs index 1e4309558339..7e40f549444d 100644 --- a/rs/embedders/tests/common/mod.rs +++ b/rs/embedders/tests/common/mod.rs @@ -2,7 +2,9 @@ use std::rc::Rc; use ic_base_types::{CanisterId, NumBytes, SubnetId}; use ic_config::{embedders::Config as EmbeddersConfig, subnet_config::SchedulerConfig}; -use ic_cycles_account_manager::{CyclesAccountManager, ResourceSaturation}; +use ic_cycles_account_manager::{ + CyclesAccountManager, CyclesAccountManagerSubnetConfig, ResourceSaturation, +}; use ic_embedders::wasmtime_embedder::system_api::{ ApiType, DefaultOutOfInstructionsHandler, ExecutionParameters, InstructionLimits, SystemApiImpl, sandbox_safe_system_state::SandboxSafeSystemState, @@ -10,6 +12,7 @@ use ic_embedders::wasmtime_embedder::system_api::{ use ic_interfaces::execution_environment::{ ExecutionMode, MessageMemoryUsage, SubnetAvailableMemory, }; +use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_logger::replica_logger::no_op_logger; use ic_nns_constants::CYCLES_MINTING_CANISTER_ID; use ic_registry_routing_table::CanisterIdRange; @@ -230,7 +233,10 @@ pub fn get_system_api( Default::default(), api_type.caller(), api_type.call_context_id(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); SystemApiImpl::new( api_type, diff --git a/rs/embedders/tests/sandbox_safe_system_state.rs b/rs/embedders/tests/sandbox_safe_system_state.rs index 9ca96572262d..0d0f03c34932 100644 --- a/rs/embedders/tests/sandbox_safe_system_state.rs +++ b/rs/embedders/tests/sandbox_safe_system_state.rs @@ -1,6 +1,7 @@ use ic_base_types::{CanisterId, NumBytes, NumSeconds, SubnetId}; use ic_config::execution_environment::SUBNET_CALLBACK_SOFT_LIMIT; use ic_config::subnet_config::SchedulerConfig; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_embedders::wasmtime_embedder::system_api::SystemApiImpl; use ic_embedders::wasmtime_embedder::system_api::sandbox_safe_system_state::SandboxSafeSystemState; use ic_interfaces::execution_environment::{HypervisorResult, MessageMemoryUsage, SystemApi}; @@ -43,13 +44,13 @@ fn push_output_request_fails_not_enough_cycles_for_request() { let cycles_account_manager = CyclesAccountManagerBuilder::new() .with_max_num_instructions(MAX_NUM_INSTRUCTIONS) .build(); + let subnet_cycles_config = CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ); let request_payload_cost = cycles_account_manager - .xnet_call_bytes_transmitted_fee( - request.payload_size_bytes(), - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - ) + .xnet_call_bytes_transmitted_fee(request.payload_size_bytes(), subnet_cycles_config) .real(); // Set cycles balance low enough that not even the cost for transferring @@ -62,16 +63,9 @@ fn push_output_request_fails_not_enough_cycles_for_request() { ); request.prepayment_for_response_execution = cycles_account_manager - .prepayment_for_response_execution( - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - WASM_EXECUTION_MODE, - ); - request.prepayment_for_call_transmission = cycles_account_manager.xnet_total_transmission_fee( - request.payload_size_bytes(), - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - ); + .prepayment_for_response_execution(subnet_cycles_config, WASM_EXECUTION_MODE); + request.prepayment_for_call_transmission = cycles_account_manager + .xnet_total_transmission_fee(request.payload_size_bytes(), subnet_cycles_config); let mut sandbox_safe_system_state = SandboxSafeSystemState::new_for_testing( &system_state, @@ -83,7 +77,7 @@ fn push_output_request_fails_not_enough_cycles_for_request() { Default::default(), Some(request.sender.into()), None, - CanisterCyclesCostSchedule::Normal, + subnet_cycles_config, ); assert_eq!( @@ -103,18 +97,15 @@ fn push_output_request_fails_not_enough_cycles_for_response() { let cycles_account_manager = CyclesAccountManagerBuilder::new() .with_max_num_instructions(MAX_NUM_INSTRUCTIONS) .build(); - - request.prepayment_for_response_execution = cycles_account_manager - .prepayment_for_response_execution( - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - WASM_EXECUTION_MODE, - ); - request.prepayment_for_call_transmission = cycles_account_manager.xnet_total_transmission_fee( - request.payload_size_bytes(), + let subnet_cycles_config = CyclesAccountManagerSubnetConfig::new( SMALL_APP_SUBNET_MAX_SIZE, CanisterCyclesCostSchedule::Normal, ); + + request.prepayment_for_response_execution = cycles_account_manager + .prepayment_for_response_execution(subnet_cycles_config, WASM_EXECUTION_MODE); + request.prepayment_for_call_transmission = cycles_account_manager + .xnet_total_transmission_fee(request.payload_size_bytes(), subnet_cycles_config); let total_cost = request.prepayment_for_response_execution.real() + request.prepayment_for_call_transmission.real(); @@ -137,7 +128,7 @@ fn push_output_request_fails_not_enough_cycles_for_response() { Default::default(), Some(request.sender.into()), None, - CanisterCyclesCostSchedule::Normal, + subnet_cycles_config, ); assert_eq!( @@ -156,6 +147,8 @@ fn push_output_request_succeeds_with_enough_cycles() { let cycles_account_manager = CyclesAccountManagerBuilder::new() .with_max_num_instructions(MAX_NUM_INSTRUCTIONS) .build(); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); let system_state = SystemState::new_running_for_testing( canister_test_id(0), @@ -175,24 +168,17 @@ fn push_output_request_succeeds_with_enough_cycles() { Default::default(), caller, None, - cost_schedule, + subnet_cycles_config, ); let mut request = OutputRequestBuilder::default().build(); request.prepayment_for_response_execution = cycles_account_manager - .prepayment_for_response_execution( - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, - WASM_EXECUTION_MODE, - ); - request.prepayment_for_response_transmission = cycles_account_manager - .prepayment_for_response_transmission(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); - request.prepayment_for_call_transmission = cycles_account_manager.xnet_total_transmission_fee( - request.payload_size_bytes(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, - ); + .prepayment_for_response_execution(subnet_cycles_config, WASM_EXECUTION_MODE); + request.prepayment_for_response_transmission = + cycles_account_manager.prepayment_for_response_transmission(subnet_cycles_config); + request.prepayment_for_call_transmission = cycles_account_manager + .xnet_total_transmission_fee(request.payload_size_bytes(), subnet_cycles_config); assert_eq!( sandbox_safe_system_state.push_output_request( @@ -212,6 +198,8 @@ fn correct_charging_source_canister_for_a_request() { .with_max_num_instructions(MAX_NUM_INSTRUCTIONS) .with_subnet_type(subnet_type) .build(); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); let mut system_state = SystemState::new_running_for_testing( canister_test_id(0), user_test_id(1).get(), @@ -236,22 +224,15 @@ fn correct_charging_source_canister_for_a_request() { Default::default(), Some(request.sender.into()), None, - CanisterCyclesCostSchedule::Normal, + subnet_cycles_config, ); request.prepayment_for_response_execution = cycles_account_manager - .prepayment_for_response_execution( - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - WASM_EXECUTION_MODE, - ); - request.prepayment_for_response_transmission = cycles_account_manager - .prepayment_for_response_transmission(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule); - request.prepayment_for_call_transmission = cycles_account_manager.xnet_total_transmission_fee( - request.payload_size_bytes(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, - ); + .prepayment_for_response_execution(subnet_cycles_config, WASM_EXECUTION_MODE); + request.prepayment_for_response_transmission = + cycles_account_manager.prepayment_for_response_transmission(subnet_cycles_config); + request.prepayment_for_call_transmission = cycles_account_manager + .xnet_total_transmission_fee(request.payload_size_bytes(), subnet_cycles_config); let total_cost = request.prepayment_for_response_execution.real() + request.prepayment_for_call_transmission.real(); @@ -288,8 +269,7 @@ fn correct_charging_source_canister_for_a_request() { &no_op_counter, &response.response_payload, request.prepayment_for_response_transmission, - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ); system_state.refund_cycles(request.prepayment_for_call_transmission, refund_cycles); @@ -301,8 +281,7 @@ fn correct_charging_source_canister_for_a_request() { + cycles_account_manager .xnet_call_bytes_transmitted_fee( MAX_INTER_CANISTER_PAYLOAD_IN_BYTES - response.payload_size_bytes(), - SMALL_APP_SUBNET_MAX_SIZE, - cost_schedule, + subnet_cycles_config, ) .real(), system_state.balance() @@ -461,7 +440,10 @@ fn is_controller_test() { Default::default(), caller, None, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); // Users IDs 1 and 2 are controllers, hence is_controller should return true, @@ -536,6 +518,10 @@ fn test_inter_canister_call( NumSeconds::from(100_000), ); + let subnet_cycles_config = CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ); let mut sandbox_safe_system_state = SandboxSafeSystemState::new_for_testing( &system_state, cycles_account_manager, @@ -546,26 +532,16 @@ fn test_inter_canister_call( Default::default(), Some(sender.into()), None, - CanisterCyclesCostSchedule::Normal, + subnet_cycles_config, ); let prepayment_for_response_execution = cycles_account_manager - .prepayment_for_response_execution( - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - WASM_EXECUTION_MODE, - ); - let prepayment_for_response_transmission = cycles_account_manager - .prepayment_for_response_transmission( - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - ); + .prepayment_for_response_execution(subnet_cycles_config, WASM_EXECUTION_MODE); + let prepayment_for_response_transmission = + cycles_account_manager.prepayment_for_response_transmission(subnet_cycles_config); let payload_size = NumBytes::from((method_name.len() + arg.len()) as u64); - let prepayment_for_call_transmission = cycles_account_manager.xnet_total_transmission_fee( - payload_size, - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - ); + let prepayment_for_call_transmission = + cycles_account_manager.xnet_total_transmission_fee(payload_size, subnet_cycles_config); let request = OutputRequestBuilder::default() .sender(sender) diff --git a/rs/embedders/tests/system_api.rs b/rs/embedders/tests/system_api.rs index 229c5e468b9d..10c1b466b581 100644 --- a/rs/embedders/tests/system_api.rs +++ b/rs/embedders/tests/system_api.rs @@ -1,6 +1,6 @@ use ic_base_types::{NumSeconds, PrincipalIdBlobParseError}; use ic_config::{embedders::Config as EmbeddersConfig, subnet_config::SchedulerConfig}; -use ic_cycles_account_manager::CyclesAccountManager; +use ic_cycles_account_manager::{CyclesAccountManager, CyclesAccountManagerSubnetConfig}; use ic_embedders::wasmtime_embedder::system_api::{ ApiType, DefaultOutOfInstructionsHandler, MAX_ENV_VAR_NAME_SIZE, SystemApiImpl, sandbox_safe_system_state::{SandboxSafeSystemState, SystemStateModifications}, @@ -1437,10 +1437,10 @@ fn call_perform_not_enough_cycles_does_not_trap() { // Set initial cycles small enough so that it does not have enough // cycles to send xnet messages. let initial_cycles = cycles_account_manager - .xnet_call_performed_fee( + .xnet_call_performed_fee(CyclesAccountManagerSubnetConfig::new( SMALL_APP_SUBNET_MAX_SIZE, CanisterCyclesCostSchedule::Normal, - ) + )) .real() - Cycles::from(10_u128); let mut system_state = SystemStateBuilder::new() @@ -1573,7 +1573,10 @@ fn growing_wasm_memory_updates_subnet_available_memory() { Default::default(), api_type.caller(), api_type.call_context_id(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); let mut api = SystemApiImpl::new( api_type, @@ -1636,7 +1639,10 @@ fn push_output_request_respects_memory_limits() { Default::default(), api_type.caller(), api_type.call_context_id(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); let own_canister_id = system_state.canister_id(); let mut api = SystemApiImpl::new( @@ -1729,7 +1735,10 @@ fn push_output_request_oversized_request_memory_limits() { Default::default(), api_type.caller(), api_type.call_context_id(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); let own_canister_id = system_state.canister_id(); let mut api = SystemApiImpl::new( @@ -2141,7 +2150,10 @@ fn get_system_api_for_best_effort_response( Default::default(), api_type.caller(), api_type.call_context_id(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); SystemApiImpl::new( diff --git a/rs/embedders/tests/wasmtime_random_memory_writes.rs b/rs/embedders/tests/wasmtime_random_memory_writes.rs index c601814ac9d6..a87065bccf21 100644 --- a/rs/embedders/tests/wasmtime_random_memory_writes.rs +++ b/rs/embedders/tests/wasmtime_random_memory_writes.rs @@ -2,7 +2,7 @@ use ic_config::{ embedders::Config as EmbeddersConfig, execution_environment::Config as HypervisorConfig, subnet_config::SchedulerConfig, }; -use ic_cycles_account_manager::ResourceSaturation; +use ic_cycles_account_manager::{CyclesAccountManagerSubnetConfig, ResourceSaturation}; use ic_embedders::{ WasmtimeEmbedder, wasm_utils::compile, @@ -14,6 +14,7 @@ use ic_embedders::{ use ic_interfaces::execution_environment::{ ExecutionMode, MessageMemoryUsage, SubnetAvailableMemory, }; +use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_logger::{ReplicaLogger, replica_logger::no_op_logger}; use ic_registry_subnet_type::SubnetType; use ic_replicated_state::{Memory, NetworkTopology, NumWasmPages}; @@ -114,7 +115,10 @@ fn test_api_for_update( Default::default(), Some(caller), api_type.call_context_id(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); let canister_current_memory_usage = NumBytes::from(0); let canister_current_message_memory_usage = MessageMemoryUsage::ZERO; diff --git a/rs/execution_environment/BUILD.bazel b/rs/execution_environment/BUILD.bazel index 9cb01fea71c0..61646ce52684 100644 --- a/rs/execution_environment/BUILD.bazel +++ b/rs/execution_environment/BUILD.bazel @@ -326,7 +326,9 @@ rust_ic_bench( deps = [ # Keep sorted. ":execution_environment", + "//rs/cycles_account_manager", "//rs/execution_environment/benches/lib:execution_environment_bench", + "//rs/limits", "//rs/monitoring/logger", "//rs/monitoring/metrics", "//rs/test_utilities/types", @@ -343,8 +345,10 @@ rust_ic_bench( deps = [ # Keep sorted. ":execution_environment", + "//rs/cycles_account_manager", "//rs/execution_environment/benches/lib:execution_environment_bench", "//rs/interfaces", + "//rs/limits", "//rs/types/cycles", "//rs/types/types", "@crate_index//:criterion", @@ -359,6 +363,7 @@ rust_ic_bench( # Keep sorted. ":execution_environment", "//packages/ic-error-types", + "//rs/cycles_account_manager", "//rs/execution_environment/benches/lib:execution_environment_bench", "//rs/limits", "//rs/types/cycles", @@ -376,6 +381,7 @@ rust_ic_bench( # Keep sorted. ":execution_environment", "//packages/ic-error-types", + "//rs/cycles_account_manager", "//rs/execution_environment/benches/lib:execution_environment_bench", "//rs/limits", "//rs/types/cycles", diff --git a/rs/execution_environment/benches/system_api/execute_inspect_message.rs b/rs/execution_environment/benches/system_api/execute_inspect_message.rs index 0a8ae0265dac..c40594628efe 100644 --- a/rs/execution_environment/benches/system_api/execute_inspect_message.rs +++ b/rs/execution_environment/benches/system_api/execute_inspect_message.rs @@ -13,7 +13,9 @@ use criterion::{Criterion, criterion_group, criterion_main}; use execution_environment_bench::{common, wat::*}; use ic_execution_environment::execution::inspect_message; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_execution_environment::{ExecutionEnvironment, IngressFilterMetrics}; +use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_logger::replica_logger::no_op_logger; use ic_metrics::MetricsRegistry; use ic_test_utilities_types::ids::user_test_id; @@ -130,7 +132,10 @@ pub fn execute_inspect_message_bench(c: &mut Criterion) { &no_op_logger(), exec_env.state_changes_error(), &IngressFilterMetrics::new(&MetricsRegistry::new()), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); assert_eq!(result, Ok(()), "Error executing inspect message method"); assert_eq!( diff --git a/rs/execution_environment/benches/system_api/execute_query.rs b/rs/execution_environment/benches/system_api/execute_query.rs index 3cfe040ae7bb..41f4a67a2f4e 100644 --- a/rs/execution_environment/benches/system_api/execute_query.rs +++ b/rs/execution_environment/benches/system_api/execute_query.rs @@ -11,11 +11,13 @@ use criterion::{Criterion, criterion_group, criterion_main}; use execution_environment_bench::{common, wat::*}; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_execution_environment::{ ExecutionEnvironment, NonReplicatedQueryKind, RoundLimits, as_num_instructions, as_round_instructions, execution::nonreplicated_query::execute_non_replicated_query, }; use ic_interfaces::execution_environment::ExecutionMode; +use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_types::PrincipalId; use ic_types::methods::WasmMethod; use ic_types_cycles::CanisterCyclesCostSchedule; @@ -130,7 +132,10 @@ pub fn execute_query_bench(c: &mut Criterion) { exec_env.hypervisor_for_testing(), &mut round_limits, exec_env.state_changes_error(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ) .2; let executed_instructions = diff --git a/rs/execution_environment/benches/system_api/execute_update.rs b/rs/execution_environment/benches/system_api/execute_update.rs index 3885bbf8d82a..33df015ffd46 100644 --- a/rs/execution_environment/benches/system_api/execute_update.rs +++ b/rs/execution_environment/benches/system_api/execute_update.rs @@ -12,6 +12,7 @@ use candid::{CandidType, Deserialize}; use criterion::{Criterion, criterion_group, criterion_main}; use execution_environment_bench::{common, wat::*}; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_error_types::ErrorCode; use ic_execution_environment::{ ExecuteMessageResult, ExecutionEnvironment, ExecutionResponse, RoundLimits, @@ -1213,8 +1214,10 @@ pub fn execute_update_bench(c: &mut Criterion) { network_topology, &mut round_limits, Default::default(), - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); let executed_instructions = as_num_instructions(instructions_before - round_limits.instructions); diff --git a/rs/execution_environment/benches/wasm_instructions/main.rs b/rs/execution_environment/benches/wasm_instructions/main.rs index 18a8431c2889..0bc9d29f0208 100644 --- a/rs/execution_environment/benches/wasm_instructions/main.rs +++ b/rs/execution_environment/benches/wasm_instructions/main.rs @@ -14,6 +14,7 @@ use std::time::Duration; use criterion::{Criterion, criterion_group, criterion_main}; use execution_environment_bench::common; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_error_types::ErrorCode; use ic_execution_environment::{ ExecuteMessageResult, ExecutionEnvironment, ExecutionResponse, RoundLimits, @@ -75,8 +76,10 @@ pub fn wasm_instructions_bench(c: &mut Criterion) { network_topology, &mut round_limits, Default::default(), - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); // We do not validate the number of executed instructions. let _executed_instructions = diff --git a/rs/execution_environment/src/canister_logs.rs b/rs/execution_environment/src/canister_logs.rs index dd116315cd14..0f56207a5aa8 100644 --- a/rs/execution_environment/src/canister_logs.rs +++ b/rs/execution_environment/src/canister_logs.rs @@ -2,7 +2,7 @@ use crate::canister_manager::types::{CanisterManagerError, CanisterManagerRespon use crate::canister_settings::VisibilitySettings; use candid::Encode; use ic_config::flag_status::FlagStatus; -use ic_cycles_account_manager::CyclesAccountManager; +use ic_cycles_account_manager::{CyclesAccountManager, CyclesAccountManagerSubnetConfig}; use ic_management_canister_types_private::{ CanisterLogRecord, FetchCanisterLogsFilter, FetchCanisterLogsRange, FetchCanisterLogsRequest, FetchCanisterLogsResponse, LogVisibilityV2, @@ -10,7 +10,6 @@ use ic_management_canister_types_private::{ use ic_replicated_state::CanisterState; use ic_types::messages::CanisterCall; use ic_types::{NumBytes, PrincipalId}; -use ic_types_cycles::CanisterCyclesCostSchedule; use std::collections::VecDeque; pub(crate) fn fetch_canister_logs( @@ -20,10 +19,9 @@ pub(crate) fn fetch_canister_logs( log_memory_store_feature: FlagStatus, msg: &mut CanisterCall, cycles_account_manager: &CyclesAccountManager, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> Result { - let max_fee = cycles_account_manager.max_fetch_canister_logs_fee(subnet_size, cost_schedule); + let max_fee = cycles_account_manager.max_fetch_canister_logs_fee(subnet_cycles_config); let payment = msg.cycles(); if payment < max_fee { return Err(CanisterManagerError::FetchCanisterLogsNotEnoughCycles { @@ -33,11 +31,10 @@ pub(crate) fn fetch_canister_logs( } let canister_id = canister.canister_id(); let reply = fetch_canister_logs_response(sender, canister, args, log_memory_store_feature)?; - msg.deduct_cycles(cycles_account_manager.fetch_canister_logs_fee( - NumBytes::new(reply.len() as u64), - subnet_size, - cost_schedule, - )); + msg.deduct_cycles( + cycles_account_manager + .fetch_canister_logs_fee(NumBytes::new(reply.len() as u64), subnet_cycles_config), + ); Ok(CanisterManagerResponse { canister_id, reply: Some(reply), diff --git a/rs/execution_environment/src/canister_manager.rs b/rs/execution_environment/src/canister_manager.rs index 428493394527..779ad266fd16 100644 --- a/rs/execution_environment/src/canister_manager.rs +++ b/rs/execution_environment/src/canister_manager.rs @@ -18,7 +18,9 @@ use crate::util::{GOVERNANCE_CANISTER_ID, MIGRATION_CANISTER_ID}; use ic_base_types::EnvironmentVariables; use ic_config::embedders::Config as EmbeddersConfig; use ic_config::flag_status::FlagStatus; -use ic_cycles_account_manager::{CyclesAccountManager, ResourceSaturation}; +use ic_cycles_account_manager::{ + CyclesAccountManager, CyclesAccountManagerSubnetConfig, ResourceSaturation, +}; use ic_embedders::wasm_utils::decoding::decode_wasm; use ic_embedders::wasmtime_embedder::system_api::{ExecutionParameters, InstructionLimits}; use ic_error_types::{ErrorCode, RejectCode, UserError}; @@ -62,8 +64,7 @@ use ic_types::{ NumBytes, NumInstructions, PrincipalId, SnapshotId, Time, }; use ic_types_cycles::{ - CanisterCreation, CanisterCyclesCostSchedule, CompoundCycles, Cycles, CyclesUseCase, - Instructions, NominalCycles, + CanisterCreation, CompoundCycles, Cycles, CyclesUseCase, Instructions, NominalCycles, }; use ic_wasm_types::WasmHash; use more_asserts::{debug_assert_ge, debug_assert_le}; @@ -336,8 +337,7 @@ impl CanisterManager { settings: &CanisterSettings, sender: PrincipalId, mut subnet_memory_saturation: ResourceSaturation, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, metrics: Option<&ExecutionEnvironmentMetrics>, ) -> Result { let mut heap_delta_increase = NumBytes::from(0); @@ -451,8 +451,7 @@ impl CanisterManager { canister.memory_usage(), canister.message_memory_usage(), canister.system_state.reserved_balance(), - subnet_size, - cost_schedule, + subnet_cycles_config, reveal_top_up, ) { @@ -512,8 +511,7 @@ impl CanisterManager { .storage_reservation_cycles( allocated_bytes, &subnet_memory_saturation, - subnet_size, - cost_schedule, + subnet_cycles_config, ) .real(); canister @@ -587,8 +585,7 @@ impl CanisterManager { - canister.log_memory_store_memory_usage() + new_log_store_memory_usage; self.cycles_and_memory_usage_checks_and_updates( - subnet_size, - cost_schedule, + subnet_cycles_config, canister, sender, log_resize_instructions, @@ -648,8 +645,7 @@ impl CanisterManager { canister: &mut CanisterState, round_limits: &mut RoundLimits, subnet_memory_saturation: ResourceSaturation, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, metrics: &ExecutionEnvironmentMetrics, ) -> Result { let sender = origin.origin(); @@ -662,8 +658,7 @@ impl CanisterManager { &settings, sender, subnet_memory_saturation, - subnet_size, - cost_schedule, + subnet_cycles_config, Some(metrics), )?; @@ -750,7 +745,6 @@ impl CanisterManager { settings: CanisterSettings, max_number_of_canisters: u64, state: &mut ReplicatedState, - subnet_size: usize, round_limits: &mut RoundLimits, subnet_memory_saturation: ResourceSaturation, canister_creation_error: &IntCounter, @@ -781,7 +775,7 @@ impl CanisterManager { let fee = self .cycles_account_manager - .canister_creation_fee(subnet_size, state.get_own_cost_schedule()); + .canister_creation_fee(state.get_own_subnet_cycles_config()); if cycles < fee.real() { return ( Err(CanisterManagerError::CreateCanisterNotEnoughCycles { @@ -809,7 +803,6 @@ impl CanisterManager { round_limits, None, subnet_memory_saturation, - subnet_size, canister_creation_error, ) { Ok(canister_id) => canister_id, @@ -852,8 +845,7 @@ impl CanisterManager { round_limits: &mut RoundLimits, compilation_cost_handling: CompilationCostHandling, round_counters: RoundCounters, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, log_dirty_pages: FlagStatus, ) -> DtsInstallCodeResult { if let Err(err) = validate_controller(&canister, &context.sender()) { @@ -881,8 +873,7 @@ impl CanisterManager { message_memory_usage, execution_parameters.compute_allocation, execution_parameters.instruction_limits.message(), - subnet_size, - cost_schedule, + subnet_cycles_config, reveal_top_up, wasm_execution_mode, ) { @@ -909,7 +900,7 @@ impl CanisterManager { prepaid_execution_cycles, time, compilation_cost_handling, - subnet_size, + subnet_cycles_config, sender: context.sender(), canister_id: canister.canister_id(), log_dirty_pages, @@ -923,7 +914,7 @@ impl CanisterManager { counters: round_counters, log: &self.log, time, - cost_schedule, + cost_schedule: subnet_cycles_config.cost_schedule, }; match context.mode { @@ -1066,8 +1057,7 @@ impl CanisterManager { &self, sender: PrincipalId, canister: &CanisterState, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ready_for_migration: bool, subnet_admins: Option>, ) -> Result { @@ -1141,8 +1131,7 @@ impl CanisterManager { canister_memory_usage, canister_message_memory_usage, compute_allocation, - subnet_size, - cost_schedule, + subnet_cycles_config, ) .get(), canister.system_state.reserved_balance().get(), @@ -1363,7 +1352,6 @@ impl CanisterManager { max_number_of_canisters: u64, round_limits: &mut RoundLimits, subnet_memory_saturation: ResourceSaturation, - subnet_size: usize, canister_creation_error: &IntCounter, ) -> Result { let sender = origin.origin(); @@ -1393,7 +1381,6 @@ impl CanisterManager { round_limits, specified_id, subnet_memory_saturation, - subnet_size, canister_creation_error, ) } @@ -1444,7 +1431,6 @@ impl CanisterManager { round_limits: &mut RoundLimits, specified_id: Option, subnet_memory_saturation: ResourceSaturation, - subnet_size: usize, canister_creation_error: &IntCounter, ) -> Result { let sender = origin.origin(); @@ -1508,8 +1494,7 @@ impl CanisterManager { &settings, sender, subnet_memory_saturation, - subnet_size, - state.get_own_cost_schedule(), + state.get_own_subnet_cycles_config(), None, ) { *round_limits = round_limits_snapshot; @@ -1678,8 +1663,7 @@ impl CanisterManager { canister: &mut CanisterState, chunk: Vec, round_limits: &mut RoundLimits, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, resource_saturation: &ResourceSaturation, consumed_cycles: &mut ConsumedCyclesForInstructions, ) -> Result { @@ -1691,18 +1675,15 @@ impl CanisterManager { // Charge for the upload. We charge before checking if the chunk has already been uploaded // since that check involves hash computation that we also want to charge for. let instructions = self.config.upload_wasm_chunk_instructions; - let cost = self.cycles_account_manager.management_canister_cost( - instructions, - subnet_size, - cost_schedule, - ); + let cost = self + .cycles_account_manager + .management_canister_cost(instructions, subnet_cycles_config); self.cycles_account_manager .consume_cycles_for_management_canister_instructions( &sender, canister, instructions, - subnet_size, - cost_schedule, + subnet_cycles_config, ) .map_err(|err| CanisterManagerError::WasmChunkStoreError { message: format!("Error charging for 'upload_chunk': {err}"), @@ -1751,8 +1732,7 @@ impl CanisterManager { let memory_usage = canister.memory_usage(); self.cycles_and_memory_usage_checks_and_updates( - subnet_size, - cost_schedule, + subnet_cycles_config, canister, sender, NumInstructions::new(0), @@ -1785,8 +1765,7 @@ impl CanisterManager { sender: PrincipalId, canister: &mut CanisterState, round_limits: &mut RoundLimits, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, resource_saturation: &ResourceSaturation, ) -> Result { // Allow the canister itself to perform this operation. @@ -1799,8 +1778,7 @@ impl CanisterManager { debug_assert_ge!(memory_usage, wasm_chunk_store_size); let new_memory_usage = memory_usage.saturating_sub(&wasm_chunk_store_size); self.cycles_and_memory_usage_checks_and_updates( - subnet_size, - cost_schedule, + subnet_cycles_config, canister, sender, NumInstructions::new(0), @@ -1849,8 +1827,7 @@ impl CanisterManager { // 4. Storage reservation cycles can be reserved. fn cycles_and_memory_usage_checks_and_updates( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, canister: &mut CanisterState, sender: PrincipalId, instructions: NumInstructions, @@ -1902,8 +1879,7 @@ impl CanisterManager { new_memory_usage, canister.message_memory_usage(), canister.system_state.reserved_balance(), - subnet_size, - cost_schedule, + subnet_cycles_config, reveal_top_up, ) { @@ -1917,7 +1893,7 @@ impl CanisterManager { // Consume cycles for instructions. let cycles_for_instructions = self .cycles_account_manager - .management_canister_cost(instructions, subnet_size, cost_schedule) + .management_canister_cost(instructions, subnet_cycles_config) .real(); let message_memory_usage = canister.message_memory_usage(); self.cycles_account_manager @@ -1925,21 +1901,19 @@ impl CanisterManager { &mut canister.system_state, new_memory_usage, message_memory_usage, - CompoundCycles::::new(cycles_for_instructions, cost_schedule), - subnet_size, - cost_schedule, + CompoundCycles::::new( + cycles_for_instructions, + subnet_cycles_config.cost_schedule, + ), + subnet_cycles_config, reveal_top_up, ) .map_err(CanisterManagerError::NotEnoughCycles)?; // Reserve cycles for storage. - let new_storage_reservation_cycles = - self.cycles_account_manager.storage_reservation_cycles( - allocated_bytes, - resource_saturation, - subnet_size, - cost_schedule, - ); + let new_storage_reservation_cycles = self + .cycles_account_manager + .storage_reservation_cycles(allocated_bytes, resource_saturation, subnet_cycles_config); canister .system_state .reserve_cycles(new_storage_reservation_cycles.real()) @@ -1981,8 +1955,7 @@ impl CanisterManager { /// If the new snapshot cannot be created, an appropriate error will be returned. pub(crate) fn take_canister_snapshot( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, origin: CanisterChangeOrigin, canister: &mut CanisterState, replace_snapshot: Option, @@ -2046,8 +2019,7 @@ impl CanisterManager { .canister_snapshot_baseline_instructions .saturating_add(&new_snapshot_size.get().into()); self.cycles_and_memory_usage_checks_and_updates( - subnet_size, - cost_schedule, + subnet_cycles_config, canister, sender, instructions, @@ -2156,8 +2128,7 @@ impl CanisterManager { #[allow(clippy::too_many_arguments)] pub(crate) fn load_canister_snapshot( &self, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, sender: PrincipalId, canister: &mut CanisterState, snapshot_canister: Arc, @@ -2241,8 +2212,7 @@ impl CanisterManager { message_memory_usage, compute_allocation, prepaid_execution_instructions, - subnet_size, - cost_schedule, + subnet_cycles_config, reveal_top_up, wasm_execution_mode, ) { @@ -2281,8 +2251,7 @@ impl CanisterManager { prepaid_execution_instructions, prepaid_execution_cycles, &metrics.execution_cycles_refund_error, - subnet_size, - cost_schedule, + subnet_cycles_config, wasm_execution_mode, &self.log, ); @@ -2292,8 +2261,7 @@ impl CanisterManager { .cycles_account_manager .variable_execution_cost( instructions_to_refund, - subnet_size, - cost_schedule, + subnet_cycles_config, wasm_execution_mode, ) .min(prepaid_execution_cycles); @@ -2416,8 +2384,7 @@ impl CanisterManager { self.config.canister_snapshot_baseline_instructions + NumInstructions::new(snapshot.size().get()); self.cycles_and_memory_usage_checks_and_updates( - subnet_size, - cost_schedule, + subnet_cycles_config, &mut new_canister, sender, instructions_for_snapshot_size, @@ -2505,8 +2472,7 @@ impl CanisterManager { canister: &mut CanisterState, delete_snapshot_id: SnapshotId, round_limits: &mut RoundLimits, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, resource_saturation: &ResourceSaturation, ) -> Result { // Check sender is a controller. @@ -2520,8 +2486,7 @@ impl CanisterManager { debug_assert_ge!(memory_usage, old_snapshot_size); let new_memory_usage = memory_usage.saturating_sub(&old_snapshot_size); self.cycles_and_memory_usage_checks_and_updates( - subnet_size, - cost_schedule, + subnet_cycles_config, canister, sender, NumInstructions::new(0), @@ -2599,8 +2564,7 @@ impl CanisterManager { canister: &mut CanisterState, snapshot_id: SnapshotId, kind: CanisterSnapshotDataKind, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, round_limits: &mut RoundLimits, consumed_cycles: &mut ConsumedCyclesForInstructions, ) -> Result { @@ -2619,15 +2583,12 @@ impl CanisterManager { &sender, canister, num_instructions, - subnet_size, - cost_schedule, + subnet_cycles_config, ) .map_err(CanisterManagerError::NotEnoughCycles)?; - let cost = self.cycles_account_manager.management_canister_cost( - num_instructions, - subnet_size, - cost_schedule, - ); + let cost = self + .cycles_account_manager + .management_canister_cost(num_instructions, subnet_cycles_config); consumed_cycles.add(cost, num_instructions); round_limits.instructions -= as_round_instructions(num_instructions); let chunk: Result, CanisterManagerError> = match kind { @@ -2693,8 +2654,7 @@ impl CanisterManager { sender: PrincipalId, canister: &mut CanisterState, args: UploadCanisterSnapshotMetadataArgs, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, round_limits: &mut RoundLimits, resource_saturation: &ResourceSaturation, time: Time, @@ -2755,8 +2715,7 @@ impl CanisterManager { .canister_snapshot_baseline_instructions .saturating_add(&new_snapshot_size.get().into()); self.cycles_and_memory_usage_checks_and_updates( - subnet_size, - cost_schedule, + subnet_cycles_config, canister, sender, instructions, @@ -2811,8 +2770,7 @@ impl CanisterManager { canister: &mut CanisterState, args: &UploadCanisterSnapshotDataArgs, round_limits: &mut RoundLimits, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, resource_saturation: &ResourceSaturation, consumed_cycles: &mut ConsumedCyclesForInstructions, ) -> Result { @@ -2847,15 +2805,12 @@ impl CanisterManager { &sender, canister, instructions, - subnet_size, - cost_schedule, + subnet_cycles_config, ) .map_err(CanisterManagerError::NotEnoughCycles)?; - let cost = self.cycles_account_manager.management_canister_cost( - instructions, - subnet_size, - cost_schedule, - ); + let cost = self + .cycles_account_manager + .management_canister_cost(instructions, subnet_cycles_config); consumed_cycles.add(cost, instructions); round_limits.instructions -= as_round_instructions(instructions); @@ -2930,8 +2885,7 @@ impl CanisterManager { let chunk_bytes = wasm_chunk_store::chunk_size(); let new_memory_usage = canister.memory_usage() + chunk_bytes; self.cycles_and_memory_usage_checks_and_updates( - subnet_size, - cost_schedule, + subnet_cycles_config, canister, sender, NumInstructions::new(0), diff --git a/rs/execution_environment/src/canister_manager/tests.rs b/rs/execution_environment/src/canister_manager/tests.rs index 4498a920b932..ed668db8dff8 100644 --- a/rs/execution_environment/src/canister_manager/tests.rs +++ b/rs/execution_environment/src/canister_manager/tests.rs @@ -36,7 +36,6 @@ use ic_embedders::{ }; use ic_error_types::{ErrorCode, RejectCode, UserError}; use ic_interfaces::execution_environment::{ExecutionMode, HypervisorError, SubnetAvailableMemory}; -use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_logger::replica_logger::no_op_logger; use ic_management_canister_types_private::{ CanisterChange, CanisterChangeDetails, CanisterChangeOrigin, CanisterIdRecord, @@ -411,8 +410,7 @@ fn install_code( round_limits, CompilationCostHandling::CountFullAmount, round_counters, - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + state.get_own_subnet_cycles_config(), Config::default().dirty_page_logging, ); // Canister manager tests do not trigger DTS executions. @@ -1584,8 +1582,7 @@ fn get_canister_status_of_stopped_canister() { .get_canister_status( sender, canister, - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + state.get_own_subnet_cycles_config(), false, subnet_admins.clone(), ) @@ -1598,8 +1595,7 @@ fn get_canister_status_of_stopped_canister() { .get_canister_status( sender, canister, - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + state.get_own_subnet_cycles_config(), true, subnet_admins, ) @@ -1621,8 +1617,7 @@ fn get_canister_status_of_stopping_canister() { .get_canister_status( sender, canister, - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + state.get_own_subnet_cycles_config(), false, subnet_admins, ) @@ -2433,7 +2428,6 @@ fn failed_upgrade_hooks_consume_instructions() { CanisterSettings::default(), MAX_NUMBER_OF_CANISTERS, &mut state, - SMALL_APP_SUBNET_MAX_SIZE, &mut round_limits, ResourceSaturation::default(), &no_op_counter(), @@ -2578,7 +2572,6 @@ fn failed_install_hooks_consume_instructions() { CanisterSettings::default(), MAX_NUMBER_OF_CANISTERS, &mut state, - SMALL_APP_SUBNET_MAX_SIZE, &mut round_limits, ResourceSaturation::default(), &no_op_counter(), @@ -2663,7 +2656,6 @@ fn install_code_respects_instruction_limit() { CanisterSettings::default(), MAX_NUMBER_OF_CANISTERS, &mut state, - SMALL_APP_SUBNET_MAX_SIZE, &mut round_limits, ResourceSaturation::default(), &no_op_counter(), @@ -3678,11 +3670,7 @@ fn unfreezing_of_frozen_canister() { assert_eq!( balance_before - balance_after, test.cycles_account_manager() - .ingress_induction_cost_from_bytes( - ingress_bytes, - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, - ) + .ingress_induction_cost_from_bytes(ingress_bytes, test.get_own_subnet_cycles_config(),) .real() ); // Now the canister works again. @@ -3935,8 +3923,7 @@ fn cycles_correct_if_upgrade_succeeds() { DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + NumInstructions::from(5 * *DROP_MEMORY_GROW_CONST_COST) + wasm_compilation_cost(&wasm), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) .real(), @@ -3960,8 +3947,7 @@ fn cycles_correct_if_upgrade_succeeds() { DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + NumInstructions::from(11 * *DROP_MEMORY_GROW_CONST_COST) + wasm_compilation_cost(&wasm), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) .real(), @@ -4002,8 +3988,7 @@ fn cycles_correct_if_upgrade_fails_at_validation() { test.canister_execution_cost(id), test.cycles_account_manager().execution_cost( DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + wasm_compilation_cost(&wasm), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) ); @@ -4026,8 +4011,7 @@ fn cycles_correct_if_upgrade_fails_at_validation() { execution_cost, test.cycles_account_manager().execution_cost( NumInstructions::from(0), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) ); @@ -4087,8 +4071,7 @@ fn cycles_correct_if_upgrade_fails_at_start() { DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + NumInstructions::from(3 * *DROP_MEMORY_GROW_CONST_COST + *UNREACHABLE_COST) + wasm_compilation_cost(&wasm2), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) .real(), @@ -4130,8 +4113,7 @@ fn cycles_correct_if_upgrade_fails_at_pre_upgrade() { test.canister_execution_cost(id), test.cycles_account_manager().execution_cost( DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + wasm_compilation_cost(&wasm), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) ); @@ -4149,8 +4131,7 @@ fn cycles_correct_if_upgrade_fails_at_pre_upgrade() { test.cycles_account_manager() .execution_cost( NumInstructions::from(3 * *DROP_MEMORY_GROW_CONST_COST + *UNREACHABLE_COST), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) .real(), @@ -4208,8 +4189,7 @@ fn cycles_correct_if_upgrade_fails_at_post_upgrade() { DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + NumInstructions::from(3 * *DROP_MEMORY_GROW_CONST_COST + *UNREACHABLE_COST) + wasm_compilation_cost(&wasm2), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) .real(), @@ -4253,8 +4233,7 @@ fn cycles_correct_if_install_succeeds() { DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + NumInstructions::from(6 * *DROP_MEMORY_GROW_CONST_COST) + wasm_compilation_cost(&wasm), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) .real(), @@ -4302,8 +4281,7 @@ fn cycles_correct_if_install_fails_at_validation() { test.canister_execution_cost(id), test.cycles_account_manager().execution_cost( NumInstructions::from(0), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) ); @@ -4348,8 +4326,7 @@ fn cycles_correct_if_install_fails_at_start() { DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + NumInstructions::from(3 * *DROP_MEMORY_GROW_CONST_COST) + wasm_compilation_cost(&wasm), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) .real(), @@ -4391,8 +4368,7 @@ fn cycles_correct_if_install_fails_at_init() { DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + NumInstructions::from(3 * *DROP_MEMORY_GROW_CONST_COST + *UNREACHABLE_COST) + wasm_compilation_cost(&wasm), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(id), ) .real(), @@ -4656,8 +4632,7 @@ fn resource_saturation_scaling_works_in_create_canister() { .storage_reservation_cycles( NumBytes::new(USAGE), &ResourceSaturation::new(subnet_memory_usage, THRESHOLD, CAPACITY), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), ) .real() ); @@ -4722,8 +4697,7 @@ fn canister_status_contains_reserved_cycles() { .storage_reservation_cycles( NumBytes::new(1_000_000), &ResourceSaturation::new(0, 0, CAPACITY), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), ) .real() .get() @@ -5711,11 +5685,7 @@ fn upload_chunk_charges_canister_cycles() { .encode(); let expected_charge = test .cycles_account_manager() - .management_canister_cost( - instructions, - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, - ) + .management_canister_cost(instructions, test.get_own_subnet_cycles_config()) .real(); let _hash = test .subnet_message("upload_chunk", payload.clone()) @@ -5758,11 +5728,7 @@ fn upload_chunk_charges_if_failing() { let instructions = SchedulerConfig::application_subnet().upload_wasm_chunk_instructions; let expected_charge = test .cycles_account_manager() - .management_canister_cost( - instructions, - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, - ) + .management_canister_cost(instructions, test.get_own_subnet_cycles_config()) .real(); // Verify we are in the expected restricted state (1022 KiB available). assert_eq!( @@ -7388,12 +7354,9 @@ fn create_canister_updates_consumed_cycles_metric_correctly() { test.ingress(canister_id, "update", payload).unwrap(); - let cycles_account_manager = Arc::new(CyclesAccountManagerBuilder::new().build()); - let creation_fee = cycles_account_manager - .canister_creation_fee( - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - ) + let creation_fee = test + .cycles_account_manager() + .canister_creation_fee(test.get_own_subnet_cycles_config()) .real(); // There's only 2 canisters on the subnet, so the one created from the first one // with have the test id corresponding to `1`. @@ -7459,9 +7422,9 @@ fn create_canister_free() { test.ingress(canister_id, "update", payload).unwrap(); - let cycles_account_manager = Arc::new(CyclesAccountManagerBuilder::new().build()); - let creation_fee = cycles_account_manager - .canister_creation_fee(SMALL_APP_SUBNET_MAX_SIZE, cost_schedule) + let creation_fee = test + .cycles_account_manager() + .canister_creation_fee(test.get_own_subnet_cycles_config()) .real(); assert_eq!(creation_fee, Cycles::new(0)); // There's only 2 canisters on the subnet, so the one created from the first one @@ -7608,7 +7571,6 @@ fn create_canister_with_cycles_sender_in_whitelist() { MAX_NUMBER_OF_CANISTERS, &mut round_limits, ResourceSaturation::default(), - SMALL_APP_SUBNET_MAX_SIZE, &no_op_counter(), ) .unwrap(); @@ -7650,7 +7612,6 @@ fn create_canister_with_specified_id( MAX_NUMBER_OF_CANISTERS, &mut round_limits, ResourceSaturation::default(), - SMALL_APP_SUBNET_MAX_SIZE, &no_op_counter(), ); @@ -8113,8 +8074,7 @@ fn create_canister_reserves_cycles_for_memory_allocation() { .storage_reservation_cycles( NumBytes::new(USAGE), &ResourceSaturation::new(subnet_memory_usage, THRESHOLD, CAPACITY), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), ) .real() ); @@ -8175,7 +8135,6 @@ fn create_canister_reverts_round_limits_on_failure() { MAX_NUMBER_OF_CANISTERS, &mut round_limits, subnet_memory_saturation, - SMALL_APP_SUBNET_MAX_SIZE, &no_op_counter(), ) .unwrap_err(); diff --git a/rs/execution_environment/src/execution/call_or_task.rs b/rs/execution_environment/src/execution/call_or_task.rs index 8f97e95e745a..29c903ef2e0a 100644 --- a/rs/execution_environment/src/execution/call_or_task.rs +++ b/rs/execution_environment/src/execution/call_or_task.rs @@ -12,6 +12,7 @@ use crate::execution_environment::{ use crate::metrics::CallTreeMetrics; use ic_base_types::CanisterId; use ic_config::flag_status::FlagStatus; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_embedders::{ wasm_executor::{CanisterStateChanges, PausedWasmExecution, WasmExecutionResult}, wasmtime_embedder::system_api::{ApiType, ExecutionParameters}, @@ -47,7 +48,7 @@ pub fn execute_call_or_task( time: Time, round: RoundContext, round_limits: &mut RoundLimits, - subnet_size: usize, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, call_tree_metrics: &dyn CallTreeMetrics, log_dirty_pages: FlagStatus, deallocation_sender: &DeallocationSender, @@ -77,8 +78,7 @@ pub fn execute_call_or_task( message_memory_usage, execution_parameters.compute_allocation, execution_parameters.instruction_limits.message(), - subnet_size, - round.cost_schedule, + subnet_cycles_config, reveal_top_up, wasm_execution_mode, ) { @@ -132,7 +132,7 @@ pub fn execute_call_or_task( call_or_task, prepaid_execution_cycles, execution_parameters, - subnet_size, + subnet_cycles_config, time, request_metadata, canister_id: clean_canister.canister_id(), @@ -198,7 +198,7 @@ pub fn execute_call_or_task( original.request_metadata.clone(), round_limits, round.network_topology, - round.cost_schedule, + original.subnet_cycles_config, ); match result { WasmExecutionResult::Paused(slice, paused_wasm_execution) => { @@ -283,8 +283,7 @@ fn finish_err( instruction_limit, original.prepaid_execution_cycles, round.counters.execution_refund_error, - original.subnet_size, - round.cost_schedule, + original.subnet_cycles_config, wasm_execution_mode, round.log, ); @@ -309,7 +308,7 @@ struct OriginalContext { prepaid_execution_cycles: CompoundCycles, method: WasmMethod, execution_parameters: ExecutionParameters, - subnet_size: usize, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, time: Time, request_metadata: RequestMetadata, canister_id: CanisterId, @@ -517,8 +516,7 @@ impl CallOrTaskHelper { new_memory_usage, new_message_memory_usage, new_reserved_balance, - original.subnet_size, - round.cost_schedule, + original.subnet_cycles_config, reveal_top_up, ) { @@ -660,8 +658,7 @@ impl CallOrTaskHelper { original.execution_parameters.instruction_limits.message(), original.prepaid_execution_cycles, round.counters.execution_refund_error, - original.subnet_size, - round.cost_schedule, + original.subnet_cycles_config, wasm_execution_mode, round.log, ); diff --git a/rs/execution_environment/src/execution/call_or_task/tests.rs b/rs/execution_environment/src/execution/call_or_task/tests.rs index e1cfbf26b03a..a90344d439f7 100644 --- a/rs/execution_environment/src/execution/call_or_task/tests.rs +++ b/rs/execution_environment/src/execution/call_or_task/tests.rs @@ -13,7 +13,7 @@ use ic_sys::PAGE_SIZE; use ic_types::ingress::IngressState; use ic_types::messages::{CallbackId, RequestMetadata}; use ic_types::{NumBytes, NumInstructions, NumOsPages}; -use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles}; +use ic_types_cycles::Cycles; use ic_universal_canister::{call_args, wasm}; use more_asserts::assert_gt; use std::time::Duration; @@ -186,8 +186,7 @@ fn dts_update_concurrent_cycles_change_succeeds() { .cycles_account_manager() .execution_cost( NumInstructions::from(instruction_limit), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(a_id), ) .real(); @@ -279,8 +278,7 @@ fn dts_replicated_query_concurrent_cycles_change_succeeds() { .cycles_account_manager() .execution_cost( NumInstructions::from(instruction_limit), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(canister_id), ) .real(); @@ -376,8 +374,7 @@ fn dts_update_concurrent_cycles_change_fails() { canister.memory_usage() + NumBytes::from(bytes_to_grow as u64), canister.message_memory_usage(), canister.compute_allocation(), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), Cycles::zero(), ); @@ -385,8 +382,7 @@ fn dts_update_concurrent_cycles_change_fails() { .cycles_account_manager() .execution_cost( NumInstructions::from(instruction_limit), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(canister_id), ) .real(); @@ -485,8 +481,7 @@ fn dts_replicated_query_concurrent_cycles_change_fails() { .cycles_account_manager() .execution_cost( NumInstructions::from(instruction_limit), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(canister_id), ) .real(); diff --git a/rs/execution_environment/src/execution/inspect_message.rs b/rs/execution_environment/src/execution/inspect_message.rs index 98ee5154aa2d..6adab093daff 100644 --- a/rs/execution_environment/src/execution/inspect_message.rs +++ b/rs/execution_environment/src/execution/inspect_message.rs @@ -3,6 +3,7 @@ use crate::{ execution_environment::{RoundLimits, as_round_instructions}, metrics::{CallTreeMetricsNoOp, IngressFilterMetrics}, }; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_embedders::wasmtime_embedder::system_api::{ApiType, ExecutionParameters}; use ic_error_types::{ErrorCode, UserError}; use ic_interfaces::execution_environment::SubnetAvailableMemory; @@ -11,7 +12,6 @@ use ic_replicated_state::{CanisterState, NetworkTopology}; use ic_types::messages::SignedIngressContent; use ic_types::methods::{FuncRef, SystemMethod, WasmMethod}; use ic_types::{NumBytes, NumInstructions, Time}; -use ic_types_cycles::CanisterCyclesCostSchedule; use prometheus::IntCounter; /// Executes the system method `canister_inspect_message`. @@ -30,7 +30,7 @@ pub fn execute_inspect_message( logger: &ReplicaLogger, state_changes_error: &IntCounter, ingress_filter_metrics: &IngressFilterMetrics, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> (NumInstructions, Result<(), UserError>) { let canister_id = canister.canister_id(); let memory_usage = canister.memory_usage(); @@ -92,7 +92,7 @@ pub fn execute_inspect_message( state_changes_error, &CallTreeMetricsNoOp, time, - cost_schedule, + subnet_cycles_config, ); drop(inspect_message_timer); ingress_filter_metrics.inspect_message_count.inc(); diff --git a/rs/execution_environment/src/execution/install.rs b/rs/execution_environment/src/execution/install.rs index 23888769a23b..65c2486d1b00 100644 --- a/rs/execution_environment/src/execution/install.rs +++ b/rs/execution_environment/src/execution/install.rs @@ -173,7 +173,7 @@ pub(crate) fn execute_install( RequestMetadata::for_new_call_tree(original.time), round_limits, round.network_topology, - round.cost_schedule, + original.subnet_cycles_config, ); match wasm_execution_result { @@ -295,7 +295,7 @@ fn install_stage_2b_continue_install_after_start( RequestMetadata::for_new_call_tree(original.time), round_limits, round.network_topology, - round.cost_schedule, + original.subnet_cycles_config, ); match wasm_execution_result { WasmExecutionResult::Finished(slice, output, canister_state_changes) => { diff --git a/rs/execution_environment/src/execution/install_code.rs b/rs/execution_environment/src/execution/install_code.rs index d5aa272f16ad..7a4d47d2554e 100644 --- a/rs/execution_environment/src/execution/install_code.rs +++ b/rs/execution_environment/src/execution/install_code.rs @@ -4,6 +4,7 @@ use crate::execution::common::{log_dirty_pages, validate_controller}; use ic_base_types::{CanisterId, NumBytes, PrincipalId}; use ic_config::flag_status::FlagStatus; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_embedders::{ wasm_executor::{CanisterStateChanges, ExecutionStateChanges}, wasmtime_embedder::system_api::ExecutionParameters, @@ -313,8 +314,7 @@ impl InstallCodeHelper { message_instruction_limit, original.prepaid_execution_cycles, round.counters.execution_refund_error, - original.subnet_size, - round.cost_schedule, + original.subnet_cycles_config, original.wasm_execution_mode, round.log, ); @@ -360,8 +360,7 @@ impl InstallCodeHelper { let reservation_cycles = round.cycles_account_manager.storage_reservation_cycles( bytes, &original.execution_parameters.subnet_memory_saturation, - original.subnet_size, - round.cost_schedule, + original.subnet_cycles_config, ); match self @@ -408,8 +407,7 @@ impl InstallCodeHelper { self.canister.memory_usage(), self.canister.message_memory_usage(), self.canister.system_state.reserved_balance(), - original.subnet_size, - round.cost_schedule, + original.subnet_cycles_config, reveal_top_up, ) { @@ -836,7 +834,7 @@ pub(crate) struct OriginalContext { pub prepaid_execution_cycles: CompoundCycles, pub time: Time, pub compilation_cost_handling: CompilationCostHandling, - pub subnet_size: usize, + pub subnet_cycles_config: CyclesAccountManagerSubnetConfig, pub sender: PrincipalId, pub canister_id: CanisterId, pub log_dirty_pages: FlagStatus, @@ -883,8 +881,7 @@ pub(crate) fn finish_err( message_instruction_limit, original.prepaid_execution_cycles, round.counters.execution_refund_error, - original.subnet_size, - round.cost_schedule, + original.subnet_cycles_config, original.wasm_execution_mode, round.log, ); diff --git a/rs/execution_environment/src/execution/install_code/tests.rs b/rs/execution_environment/src/execution/install_code/tests.rs index 2a05dc4ce0e7..015e8b26710c 100644 --- a/rs/execution_environment/src/execution/install_code/tests.rs +++ b/rs/execution_environment/src/execution/install_code/tests.rs @@ -23,7 +23,7 @@ use ic_test_utilities_metrics::fetch_int_counter; use ic_types::ingress::{IngressState, IngressStatus, WasmResult}; use ic_types::messages::MessageId; use ic_types::{CanisterId, ComputeAllocation, MemoryAllocation, NumBytes, NumInstructions}; -use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles}; +use ic_types_cycles::Cycles; use ic_types_test_utils::ids::{canister_test_id, subnet_test_id, user_test_id}; use ic_universal_canister::{UNIVERSAL_CANISTER_WASM, call_args, wasm}; use maplit::btreemap; @@ -100,8 +100,7 @@ fn dts_resume_works_in_install_code() { .cycles_account_manager() .execution_cost( NumInstructions::from(INSTRUCTION_LIMIT), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WASM_EXECUTION_MODE, ) .real(), @@ -154,8 +153,7 @@ fn dts_abort_works_in_install_code() { .cycles_account_manager() .execution_cost( NumInstructions::from(INSTRUCTION_LIMIT), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WASM_EXECUTION_MODE ) .real(), @@ -181,8 +179,7 @@ fn dts_abort_works_in_install_code() { .cycles_account_manager() .execution_cost( NumInstructions::from(INSTRUCTION_LIMIT), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WASM_EXECUTION_MODE ) .real(), @@ -441,8 +438,7 @@ fn execute_install_code_message_dts_helper( .cycles_account_manager() .execution_cost( NumInstructions::from(1_000_000), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WASM_EXECUTION_MODE ) .real(), @@ -644,8 +640,7 @@ fn reserve_cycles_for_execution_fails_when_not_enough_cycles() { NumBytes::new(canister_history_memory_usage as u64 + canister_log_memory_store_usage), MessageMemoryUsage::ZERO, ComputeAllocation::zero(), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), Cycles::zero(), ); let canister_id = test.create_canister(Cycles::new(900_000) + freezing_threshold_cycles); @@ -2346,8 +2341,7 @@ fn failed_install_chunked_charges_for_wasm_assembly() { .cycles_account_manager() .execution_cost( NumInstructions::from(wasm_chunk_store::chunk_size().get()), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WASM_EXECUTION_MODE, ) .real(); @@ -2426,8 +2420,7 @@ fn successful_install_chunked_charges_for_wasm_assembly() { .cycles_account_manager() .execution_cost( NumInstructions::from(0), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WASM_EXECUTION_MODE, ) .real(); @@ -2435,8 +2428,7 @@ fn successful_install_chunked_charges_for_wasm_assembly() { .cycles_account_manager() .execution_cost( NumInstructions::from(wasm_chunk_store::chunk_size().get()), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WASM_EXECUTION_MODE, ) .real() diff --git a/rs/execution_environment/src/execution/nonreplicated_query.rs b/rs/execution_environment/src/execution/nonreplicated_query.rs index 622461b4ba6d..35293ebaae0e 100644 --- a/rs/execution_environment/src/execution/nonreplicated_query.rs +++ b/rs/execution_environment/src/execution/nonreplicated_query.rs @@ -8,6 +8,7 @@ use crate::execution::common::{validate_canister, validate_method}; use crate::execution_environment::RoundLimits; use crate::{Hypervisor, NonReplicatedQueryKind, metrics::CallTreeMetricsNoOp}; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_embedders::wasmtime_embedder::system_api::{ApiType, ExecutionParameters}; use ic_error_types::UserError; use ic_interfaces::execution_environment::SystemApiCallCounters; @@ -16,7 +17,7 @@ use ic_types::ingress::WasmResult; use ic_types::messages::{CallContextId, RequestMetadata}; use ic_types::methods::{FuncRef, WasmMethod}; use ic_types::{NumInstructions, Time}; -use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles}; +use ic_types_cycles::Cycles; use prometheus::IntCounter; // Execute non replicated query. @@ -33,7 +34,7 @@ pub fn execute_non_replicated_query( hypervisor: &Hypervisor, round_limits: &mut RoundLimits, state_changes_error: &IntCounter, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> ( CanisterState, NumInstructions, @@ -135,7 +136,7 @@ pub fn execute_non_replicated_query( state_changes_error, &CallTreeMetricsNoOp, time, - cost_schedule, + subnet_cycles_config, ); canister.system_state = output_system_state; if preserve_changes { diff --git a/rs/execution_environment/src/execution/response.rs b/rs/execution_environment/src/execution/response.rs index fc397cf1b2dc..86c4ed83a4e7 100644 --- a/rs/execution_environment/src/execution/response.rs +++ b/rs/execution_environment/src/execution/response.rs @@ -7,6 +7,7 @@ use ic_base_types::CanisterId; use ic_limits::LOG_CANISTER_OPERATION_CYCLES_THRESHOLD; use more_asserts::debug_assert_le; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_embedders::{ wasm_executor::{ CanisterStateChanges, PausedWasmExecution, WasmExecutionResult, wasm_execution_error, @@ -26,10 +27,7 @@ use ic_types::messages::{ }; use ic_types::methods::{Callback, FuncRef, WasmClosure}; use ic_types::{NumBytes, NumInstructions, Time}; -use ic_types_cycles::{ - CanisterCyclesCostSchedule, CompoundCycles, Cycles, Instructions, - RequestAndResponseTransmission, -}; +use ic_types_cycles::{CompoundCycles, Cycles, Instructions, RequestAndResponseTransmission}; use ic_utils_thread::deallocator_thread::DeallocationSender; use ic_wasm_types::WasmEngineError::FailedToApplySystemChanges; @@ -179,8 +177,7 @@ impl ResponseHelper { round.counters.response_cycles_refund_error, &response.response_payload, original.callback.prepayment_for_response_transmission, - original.subnet_size, - round.cost_schedule, + original.subnet_cycles_config, ); let canister = clean_canister.clone(); @@ -601,8 +598,7 @@ impl ResponseHelper { original.message_instruction_limit, original.callback.prepayment_for_response_execution, round.counters.execution_refund_error, - original.subnet_size, - round.cost_schedule, + original.subnet_cycles_config, wasm_execution_mode, round.log, ); @@ -701,11 +697,10 @@ struct OriginalContext { request_metadata: RequestMetadata, message_instruction_limit: NumInstructions, message: Arc, - subnet_size: usize, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, canister_id: CanisterId, instructions_executed: NumInstructions, log_dirty_pages: FlagStatus, - cost_schedule: CanisterCyclesCostSchedule, /// Sender info from the ingress message that created the call context. /// `None` for call contexts created by inter-canister calls. sender_info: Option, @@ -820,7 +815,10 @@ impl PausedExecution for PausedResponseExecution { // No cycles were prepaid for execution during this DTS execution. ( CanisterMessageOrTask::Message(message), - CompoundCycles::new(Cycles::zero(), self.original.cost_schedule), + CompoundCycles::new( + Cycles::zero(), + self.original.subnet_cycles_config.cost_schedule, + ), ) } @@ -932,7 +930,10 @@ impl PausedExecution for PausedCleanupExecution { // No cycles were prepaid for execution during this DTS execution. ( CanisterMessageOrTask::Message(message), - CompoundCycles::new(Cycles::zero(), self.original.cost_schedule), + CompoundCycles::new( + Cycles::zero(), + self.original.subnet_cycles_config.cost_schedule, + ), ) } @@ -958,7 +959,7 @@ pub fn execute_response( execution_parameters: ExecutionParameters, round: RoundContext, round_limits: &mut RoundLimits, - subnet_size: usize, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, call_tree_metrics: &dyn CallTreeMetrics, log_dirty_pages: FlagStatus, deallocation_sender: &DeallocationSender, @@ -993,11 +994,10 @@ pub fn execute_response( request_metadata: call_context.metadata().clone(), message_instruction_limit: execution_parameters.instruction_limits.message(), message: Arc::clone(&response), - subnet_size, + subnet_cycles_config, canister_id: clean_canister.canister_id(), instructions_executed: call_context.instructions_executed(), log_dirty_pages, - cost_schedule: round.cost_schedule, sender_info: call_context.sender_info().cloned(), }; @@ -1066,7 +1066,7 @@ pub fn execute_response( original.request_metadata.clone(), round_limits, round.network_topology, - round.cost_schedule, + original.subnet_cycles_config, ); process_response_result( @@ -1144,7 +1144,7 @@ fn execute_response_cleanup( original.request_metadata.clone(), round_limits, round.network_topology, - round.cost_schedule, + original.subnet_cycles_config, ); process_cleanup_result( result, diff --git a/rs/execution_environment/src/execution/response/tests.rs b/rs/execution_environment/src/execution/response/tests.rs index 37fd5d15f28a..f44f454a2a7c 100644 --- a/rs/execution_environment/src/execution/response/tests.rs +++ b/rs/execution_environment/src/execution/response/tests.rs @@ -164,11 +164,10 @@ fn execute_response_refunds_cycles() { // Compute the response transmission refund. let mgr = test.cycles_account_manager(); let prepayment_for_response_transmission = - mgr.prepayment_for_response_transmission(test.subnet_size(), cost_schedule); + mgr.prepayment_for_response_transmission(test.get_own_subnet_cycles_config()); let actual_response_transmission_fee = mgr.xnet_call_bytes_transmitted_fee( NumBytes::from(b_callback.len() as u64), - test.subnet_size(), - cost_schedule, + test.get_own_subnet_cycles_config(), ); let response_transmission_refund = prepayment_for_response_transmission - actual_response_transmission_fee; @@ -1408,8 +1407,7 @@ fn dts_response_concurrent_cycles_change_succeeds() { .cycles_account_manager() .execution_cost( NumInstructions::from(instruction_limit), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(a_id), ) .real(); @@ -1533,8 +1531,7 @@ fn dts_response_concurrent_cycles_change_fails() { .cycles_account_manager() .execution_cost( NumInstructions::from(instruction_limit), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(a_id), ) .real(); @@ -1681,8 +1678,7 @@ fn dts_response_with_cleanup_concurrent_cycles_change_succeeds() { .cycles_account_manager() .execution_cost( NumInstructions::from(instruction_limit), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(a_id), ) .real(); @@ -2823,8 +2819,7 @@ fn test_cycles_burn() { canister_memory_usage, canister_message_memory_usage, ComputeAllocation::zero(), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), Cycles::zero(), ); @@ -2845,8 +2840,7 @@ fn cycles_burn_up_to_the_threshold_on_not_enough_cycles() { canister_memory_usage, canister_message_memory_usage, ComputeAllocation::zero(), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), Cycles::zero(), ); @@ -2861,8 +2855,7 @@ fn cycles_burn_up_to_the_threshold_on_not_enough_cycles() { canister_memory_usage, canister_message_memory_usage, ComputeAllocation::zero(), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), Cycles::zero(), ); diff --git a/rs/execution_environment/src/execution/upgrade.rs b/rs/execution_environment/src/execution/upgrade.rs index 6e64ec3b0e07..6edd5e40baf7 100644 --- a/rs/execution_environment/src/execution/upgrade.rs +++ b/rs/execution_environment/src/execution/upgrade.rs @@ -164,7 +164,7 @@ pub(crate) fn execute_upgrade( RequestMetadata::for_new_call_tree(original.time), round_limits, round.network_topology, - round.cost_schedule, + original.subnet_cycles_config, ); match wasm_execution_result { @@ -362,7 +362,7 @@ fn upgrade_stage_2_and_3a_create_execution_state_and_call_start( RequestMetadata::for_new_call_tree(original.time), round_limits, round.network_topology, - round.cost_schedule, + original.subnet_cycles_config, ); match wasm_execution_result { @@ -487,7 +487,7 @@ fn upgrade_stage_4a_call_post_upgrade( RequestMetadata::for_new_call_tree(original.time), round_limits, round.network_topology, - round.cost_schedule, + original.subnet_cycles_config, ); match wasm_execution_result { WasmExecutionResult::Finished(slice, output, canister_state_changes) => { diff --git a/rs/execution_environment/src/execution/upgrade/tests.rs b/rs/execution_environment/src/execution/upgrade/tests.rs index 367986ea2d40..2f95e174fa00 100644 --- a/rs/execution_environment/src/execution/upgrade/tests.rs +++ b/rs/execution_environment/src/execution/upgrade/tests.rs @@ -12,7 +12,7 @@ use ic_test_utilities_metrics::fetch_int_counter; use ic_test_utilities_types::ids::user_test_id; use ic_types::ingress::IngressState; use ic_types::{ComputeAllocation, MemoryAllocation}; -use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles}; +use ic_types_cycles::Cycles; //////////////////////////////////////////////////////////////////////// // Constants and templates @@ -217,8 +217,7 @@ fn upgrade_fails_on_not_enough_cycles() { // Should be enough cycles to create the canister, but not enough to upgrade it let balance_cycles = test.cycles_account_manager().execution_cost( (MAX_INSTRUCTIONS_PER_SLICE * 3).into(), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WasmExecutionMode::Wasm32, ); @@ -237,8 +236,7 @@ fn upgrade_fails_on_not_enough_cycles() { canister_memory_usage, canister_message_memory_usage, ComputeAllocation::zero(), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), Cycles::zero(), ); let canister_id = test diff --git a/rs/execution_environment/src/execution_environment.rs b/rs/execution_environment/src/execution_environment.rs index f25a304c1b1a..e947f3e0c1ba 100644 --- a/rs/execution_environment/src/execution_environment.rs +++ b/rs/execution_environment/src/execution_environment.rs @@ -20,7 +20,10 @@ use ic_base_types::PrincipalId; use ic_config::execution_environment::Config as ExecutionConfig; use ic_config::flag_status::FlagStatus; use ic_crypto_utils_canister_threshold_sig::derive_threshold_public_key; -use ic_cycles_account_manager::{CyclesAccountManager, IngressInductionCost, ResourceSaturation}; +use ic_cycles_account_manager::{ + CyclesAccountManager, CyclesAccountManagerSubnetConfig, IngressInductionCost, + ResourceSaturation, +}; use ic_embedders::wasmtime_embedder::system_api::{ExecutionParameters, InstructionLimits}; use ic_error_types::{ErrorCode, RejectCode, UserError}; use ic_interfaces::execution_environment::{ @@ -337,8 +340,7 @@ impl<'a> ConsumedCyclesForInstructions<'a> { self, canister: &mut CanisterState, round_limits: &mut RoundLimits, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, failed_charge: &IntCounter, ) { let memory_usage = canister.memory_usage(); @@ -348,8 +350,7 @@ impl<'a> ConsumedCyclesForInstructions<'a> { memory_usage, message_memory_usage, self.consumed_cycles, - subnet_size, - cost_schedule, + subnet_cycles_config, true, /* we only log the error, but do not return it to the user => do reveal top up balance */ ); if let Err(err) = res { @@ -614,7 +615,6 @@ impl ExecutionEnvironment { state: &mut ReplicatedState, msg: &mut CanisterCall, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult where @@ -626,6 +626,7 @@ impl ExecutionEnvironment { ) -> Result, { let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let mut consumed_cycles = ConsumedCyclesForInstructions::new( &self.cycles_account_manager, cost_schedule, @@ -650,8 +651,7 @@ impl ExecutionEnvironment { consumed_cycles.apply( canister, round_limits, - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, &self.metrics.failed_subnet_message_charge, ); self.process_canister_manager_result(Err(err), state, msg, current_round) @@ -690,7 +690,6 @@ impl ExecutionEnvironment { round_limits: &mut RoundLimits, ) -> (ReplicatedState, ExecuteSubnetMessageResultType) { let since = Instant::now(); // Start logging execution time. - let cost_schedule = state.get_own_cost_schedule(); let mut msg = match msg { SubnetMessage::Response(response) => { @@ -709,15 +708,13 @@ impl ExecutionEnvironment { let old_price = self.cycles_account_manager.http_request_fee( context.variable_parts_size(), context.max_response_bytes, - registry_settings.subnet_size, - cost_schedule, + state.get_own_subnet_cycles_config(), ); let new_price = self.cycles_account_manager.http_request_fee_beta( context.variable_parts_size(), context.max_response_bytes, - registry_settings.subnet_size, - cost_schedule, + state.get_own_subnet_cycles_config(), NumBytes::from(response.payload_size_bytes()), ); @@ -821,7 +818,6 @@ impl ExecutionEnvironment { state, instruction_limits, round_limits, - registry_settings.subnet_size, current_round, ); } @@ -837,7 +833,6 @@ impl ExecutionEnvironment { state, instruction_limits, round_limits, - registry_settings.subnet_size, current_round, ); } @@ -895,7 +890,6 @@ impl ExecutionEnvironment { .map(|setting| setting.max_queue_size) .unwrap_or_default(), &mut state, - registry_settings.subnet_size, ) { Err(err) => ExecuteSubnetMessageResult::Finished { response: Err(err), @@ -988,7 +982,6 @@ impl ExecutionEnvironment { round_limits, subnet_admins, time, - registry_settings, current_round, ) } @@ -1020,7 +1013,6 @@ impl ExecutionEnvironment { &mut state, &mut msg, round_limits, - registry_settings, current_round, ), }; @@ -1029,7 +1021,7 @@ impl ExecutionEnvironment { // decrease the freezing threshold if it was set too // high that topping up the canister is not feasible. if let CanisterCall::Ingress(ingress) = &msg { - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); if let Ok(canister) = canister_make_mut(canister_id, &mut state) && self .cycles_account_manager @@ -1041,8 +1033,7 @@ impl ExecutionEnvironment { .cycles_account_manager .ingress_induction_cost_from_bytes( NumBytes::from(bytes_to_charge as u64), - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, ); let memory_usage = canister.memory_usage(); let message_memory_usage = canister.message_memory_usage(); @@ -1053,8 +1044,7 @@ impl ExecutionEnvironment { memory_usage, message_memory_usage, induction_cost, - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, false, // we ignore the error anyway => no need to reveal top up balance ); } @@ -1079,7 +1069,6 @@ impl ExecutionEnvironment { *msg.sender(), args.get_canister_id(), &state, - registry_settings.subnet_size, ready_for_migration, subnet_admins, ) @@ -1146,7 +1135,6 @@ impl ExecutionEnvironment { &mut msg, subnet_admins, round_limits, - registry_settings, current_round, ) } @@ -1165,7 +1153,6 @@ impl ExecutionEnvironment { &mut state, subnet_admins, round_limits, - registry_settings, current_round, ) } @@ -1273,7 +1260,6 @@ impl ExecutionEnvironment { canister_http_request_context, &mut state, request.as_ref(), - registry_settings, since, ) { Err(err) => ExecuteSubnetMessageResult::Finished { @@ -1468,7 +1454,6 @@ impl ExecutionEnvironment { .map(|setting| setting.max_queue_size) .unwrap_or_default(), &mut state, - registry_settings.subnet_size, ) { Err(err) => ExecuteSubnetMessageResult::Finished { response: Err(err), @@ -1617,7 +1602,6 @@ impl ExecutionEnvironment { registry_settings.max_number_of_canisters, round_limits, saturation, - registry_settings.subnet_size, &self.metrics.canister_creation_error, ) .map(|canister_id| { @@ -1651,7 +1635,6 @@ impl ExecutionEnvironment { &mut msg, ®istry_settings.provisional_whitelist, round_limits, - registry_settings, current_round, ), } @@ -1731,7 +1714,6 @@ impl ExecutionEnvironment { &mut msg, args, round_limits, - registry_settings, &resource_saturation, current_round, ), @@ -1754,7 +1736,6 @@ impl ExecutionEnvironment { &mut msg, args, round_limits, - registry_settings, &resource_saturation, current_round, ) @@ -1825,7 +1806,7 @@ impl ExecutionEnvironment { }, Ok(args) => { let canister_id = args.get_canister_id(); - let subnet_size = registry_settings.subnet_size; + let subnet_cycles_config = state.get_own_subnet_cycles_config(); self.execute_mgmt_operation_on_canister( canister_id, |canister, msg, _round_limits, _consumed_cycles| { @@ -1836,14 +1817,12 @@ impl ExecutionEnvironment { self.config.log_memory_store_feature, msg, &self.cycles_account_manager, - subnet_size, - cost_schedule, + subnet_cycles_config, ) }, &mut state, &mut msg, round_limits, - registry_settings, current_round, ) } @@ -1865,7 +1844,6 @@ impl ExecutionEnvironment { &mut msg, args, round_limits, - registry_settings, current_round, ), }, @@ -1886,7 +1864,6 @@ impl ExecutionEnvironment { round_limits, instruction_limits, origin, - registry_settings, current_round, ) } @@ -1921,7 +1898,6 @@ impl ExecutionEnvironment { &mut msg, args, round_limits, - registry_settings, &resource_saturation, current_round, ) @@ -1959,7 +1935,6 @@ impl ExecutionEnvironment { &mut msg, args, round_limits, - registry_settings, current_round, ), } @@ -1977,7 +1952,6 @@ impl ExecutionEnvironment { &mut msg, args, round_limits, - registry_settings, current_round, ), } @@ -1995,7 +1969,6 @@ impl ExecutionEnvironment { &mut msg, args, round_limits, - registry_settings, current_round, ), } @@ -2112,14 +2085,12 @@ impl ExecutionEnvironment { mut canister_http_request_context: CanisterHttpRequestContext, state: &mut ReplicatedState, request: &Request, - registry_settings: &RegistryExecutionSettings, since: Instant, ) -> Result<(), UserError> { let http_request_fee = self.cycles_account_manager.http_request_fee( canister_http_request_context.variable_parts_size(), canister_http_request_context.max_response_bytes, - registry_settings.subnet_size, - state.get_own_cost_schedule(), + state.get_own_subnet_cycles_config(), ); let real_http_request_fee = http_request_fee.real(); let nominal_http_request_fee = http_request_fee.nominal(); @@ -2241,8 +2212,7 @@ impl ExecutionEnvironment { network_topology: Arc, round_limits: &mut RoundLimits, resource_limits: ResourceLimits, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> ExecuteMessageResult { if canister.has_long_execution_or_install_code() { panic!( @@ -2269,7 +2239,7 @@ impl ExecutionEnvironment { counters: round_counters, log: &self.log, time, - cost_schedule, + cost_schedule: subnet_cycles_config.cost_schedule, }; let req = match input { @@ -2282,7 +2252,7 @@ impl ExecutionEnvironment { round, round_limits, resource_limits, - subnet_size, + subnet_cycles_config, ); } CanisterMessageOrTask::Message(CanisterMessage::Response { response, callback }) => { @@ -2295,8 +2265,7 @@ impl ExecutionEnvironment { network_topology, round_limits, resource_limits, - subnet_size, - cost_schedule, + subnet_cycles_config, ); } CanisterMessageOrTask::Message(CanisterMessage::Request(request)) => { @@ -2346,7 +2315,7 @@ impl ExecutionEnvironment { time, round, round_limits, - subnet_size, + subnet_cycles_config, &self.call_tree_metrics, self.config.dirty_page_logging, self.deallocator_thread.sender(), @@ -2382,7 +2351,7 @@ impl ExecutionEnvironment { time, round, round_limits, - subnet_size, + subnet_cycles_config, &self.call_tree_metrics, self.config.dirty_page_logging, self.deallocator_thread.sender(), @@ -2404,7 +2373,7 @@ impl ExecutionEnvironment { round: RoundContext, round_limits: &mut RoundLimits, resource_limits: ResourceLimits, - subnet_size: usize, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> ExecuteMessageResult { let execution_parameters = self.execution_parameters( &canister, @@ -2421,7 +2390,7 @@ impl ExecutionEnvironment { round.time, round, round_limits, - subnet_size, + subnet_cycles_config, &self.call_tree_metrics, self.config.dirty_page_logging, self.deallocator_thread.sender(), @@ -2471,7 +2440,6 @@ impl ExecutionEnvironment { settings, registry_settings.max_number_of_canisters, state, - registry_settings.subnet_size, round_limits, self.subnet_memory_saturation(&round_limits.subnet_available_memory, resource_limits), &self.metrics.canister_creation_error, @@ -2496,15 +2464,13 @@ impl ExecutionEnvironment { state: &mut ReplicatedState, msg: &mut CanisterCall, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let saturation = self.subnet_memory_saturation( &round_limits.subnet_available_memory, state.resource_limits(), ); - let subnet_size = registry_settings.subnet_size; self.execute_mgmt_operation_on_canister( canister_id, |canister, _msg, round_limits, _consumed_cycles| { @@ -2515,15 +2481,13 @@ impl ExecutionEnvironment { canister, round_limits, saturation, - subnet_size, - cost_schedule, + subnet_cycles_config, &self.metrics, ) }, state, msg, round_limits, - registry_settings, current_round, ) } @@ -2537,7 +2501,6 @@ impl ExecutionEnvironment { round_limits: &mut RoundLimits, subnet_admins: Option>, time: Time, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { self.execute_mgmt_operation_on_canister( @@ -2554,7 +2517,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ) } @@ -2567,7 +2529,6 @@ impl ExecutionEnvironment { msg: &mut CanisterCall, subnet_admins: Option>, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { self.execute_mgmt_operation_on_canister( @@ -2579,7 +2540,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ) } @@ -2619,18 +2579,15 @@ impl ExecutionEnvironment { sender: PrincipalId, canister_id: CanisterId, state: &ReplicatedState, - subnet_size: usize, ready_for_migration: bool, subnet_admins: Option>, ) -> Result, UserError> { - let cost_schedule = state.get_own_cost_schedule(); let canister = get_canister(canister_id, state)?; self.canister_manager .get_canister_status( sender, canister, - subnet_size, - cost_schedule, + state.get_own_subnet_cycles_config(), ready_for_migration, subnet_admins, ) @@ -2699,7 +2656,6 @@ impl ExecutionEnvironment { state: &mut ReplicatedState, subnet_admins: Option>, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { let call_id = state @@ -2719,7 +2675,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ); if let ExecuteSubnetMessageResult::Finished { @@ -2740,7 +2695,6 @@ impl ExecutionEnvironment { msg: &mut CanisterCall, provisional_whitelist: &ProvisionalWhitelist, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { self.execute_mgmt_operation_on_canister( @@ -2752,7 +2706,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ) } @@ -2764,11 +2717,10 @@ impl ExecutionEnvironment { msg: &mut CanisterCall, args: UploadChunkArgs, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, resource_saturation: &ResourceSaturation, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let canister_id = args.get_canister_id(); let chunk = args.chunk; self.execute_mgmt_operation_on_canister( @@ -2779,8 +2731,7 @@ impl ExecutionEnvironment { canister, chunk, round_limits, - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, resource_saturation, consumed_cycles, ) @@ -2788,7 +2739,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ) } @@ -2800,11 +2750,10 @@ impl ExecutionEnvironment { msg: &mut CanisterCall, args: ClearChunkStoreArgs, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, resource_saturation: &ResourceSaturation, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let canister_id = args.get_canister_id(); self.execute_mgmt_operation_on_canister( canister_id, @@ -2813,15 +2762,13 @@ impl ExecutionEnvironment { sender, canister, round_limits, - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, resource_saturation, ) }, state, msg, round_limits, - registry_settings, current_round, ) } @@ -2847,11 +2794,10 @@ impl ExecutionEnvironment { msg: &mut CanisterCall, args: TakeCanisterSnapshotArgs, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { let canister_id = args.get_canister_id(); - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let resource_saturation = self.subnet_memory_saturation( &round_limits.subnet_available_memory, state.resource_limits(), @@ -2863,8 +2809,7 @@ impl ExecutionEnvironment { canister_id, |canister, _msg, round_limits, _consumed_cycles| { self.canister_manager.take_canister_snapshot( - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, origin, canister, replace_snapshot, @@ -2877,7 +2822,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ) } @@ -2892,7 +2836,6 @@ impl ExecutionEnvironment { round_limits: &mut RoundLimits, instruction_limits: InstructionLimits, origin: CanisterChangeOrigin, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { // Check if the canister on which the snapshot is loaded exists. @@ -2931,7 +2874,7 @@ impl ExecutionEnvironment { } }; - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let resource_saturation = self.subnet_memory_saturation( &round_limits.subnet_available_memory, state.resource_limits(), @@ -2942,8 +2885,7 @@ impl ExecutionEnvironment { canister_id, |canister, _msg, round_limits, consumed_cycles| { self.canister_manager.load_canister_snapshot( - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, sender, canister, snapshot_canister, @@ -2961,7 +2903,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ) } @@ -2988,12 +2929,11 @@ impl ExecutionEnvironment { msg: &mut CanisterCall, args: DeleteCanisterSnapshotArgs, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, resource_saturation: &ResourceSaturation, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { let canister_id = args.get_canister_id(); - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); self.execute_mgmt_operation_on_canister( canister_id, |canister, _msg, round_limits, _consumed_cycles| { @@ -3002,15 +2942,13 @@ impl ExecutionEnvironment { canister, args.get_snapshot_id(), round_limits, - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, resource_saturation, ) }, state, msg, round_limits, - registry_settings, current_round, ) } @@ -3022,11 +2960,10 @@ impl ExecutionEnvironment { msg: &mut CanisterCall, args: ReadCanisterSnapshotDataArgs, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { let canister_id = args.get_canister_id(); - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); self.execute_mgmt_operation_on_canister( canister_id, |canister, _msg, round_limits, consumed_cycles| { @@ -3035,8 +2972,7 @@ impl ExecutionEnvironment { canister, args.get_snapshot_id(), args.kind, - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, round_limits, consumed_cycles, ) @@ -3044,7 +2980,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ) } @@ -3116,11 +3051,10 @@ impl ExecutionEnvironment { msg: &mut CanisterCall, args: UploadCanisterSnapshotMetadataArgs, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { let canister_id = args.get_canister_id(); - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let resource_saturation = self.subnet_memory_saturation( &round_limits.subnet_available_memory, state.resource_limits(), @@ -3133,8 +3067,7 @@ impl ExecutionEnvironment { sender, canister, args, - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, round_limits, &resource_saturation, time, @@ -3143,7 +3076,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ) } @@ -3155,11 +3087,10 @@ impl ExecutionEnvironment { msg: &mut CanisterCall, args: UploadCanisterSnapshotDataArgs, round_limits: &mut RoundLimits, - registry_settings: &RegistryExecutionSettings, current_round: ExecutionRound, ) -> ExecuteSubnetMessageResult { let canister_id = args.get_canister_id(); - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let resource_saturation = self.subnet_memory_saturation( &round_limits.subnet_available_memory, state.resource_limits(), @@ -3172,8 +3103,7 @@ impl ExecutionEnvironment { canister, &args, round_limits, - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, &resource_saturation, consumed_cycles, ) @@ -3181,7 +3111,6 @@ impl ExecutionEnvironment { state, msg, round_limits, - registry_settings, current_round, ) } @@ -3247,8 +3176,7 @@ impl ExecutionEnvironment { network_topology: Arc, round_limits: &mut RoundLimits, resource_limits: ResourceLimits, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> ExecuteMessageResult { let execution_parameters = self.execution_parameters( &canister, @@ -3275,7 +3203,7 @@ impl ExecutionEnvironment { counters: round_counters, log: &self.log, time, - cost_schedule, + cost_schedule: subnet_cycles_config.cost_schedule, }; execute_response( canister, @@ -3285,7 +3213,7 @@ impl ExecutionEnvironment { execution_parameters, round, round_limits, - subnet_size, + subnet_cycles_config, &self.call_tree_metrics, self.config.dirty_page_logging, self.deallocator_thread.sender(), @@ -3318,12 +3246,11 @@ impl ExecutionEnvironment { // if the canister's balance is too low. A more rigorous check happens later // in the ingress selector. { - let subnet_size = state.get_own_subnet_size(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let induction_cost = self.cycles_account_manager.ingress_induction_cost( ingress, effective_canister_id, - subnet_size, - state.get_own_cost_schedule(), + subnet_cycles_config, ); if let IngressInductionCost::Fee { payer, cost } = induction_cost { @@ -3339,8 +3266,7 @@ impl ExecutionEnvironment { paying_canister.memory_usage(), paying_canister.message_memory_usage(), paying_canister.system_state.reserved_balance(), - subnet_size, - state.get_own_cost_schedule(), + subnet_cycles_config, reveal_top_up, ) { @@ -3418,7 +3344,7 @@ impl ExecutionEnvironment { &self.log, &self.metrics.state_changes_error, metrics, - state.get_own_cost_schedule(), + state.get_own_subnet_cycles_config(), ) .1 } @@ -3708,31 +3634,28 @@ impl ExecutionEnvironment { .map(|setting| setting.max_queue_size) .unwrap_or_default(), state, - registry_settings.subnet_size, ) } fn calculate_signature_fee( &self, args: &ThresholdArguments, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> ThresholdSignatureCycles { let cam = &self.cycles_account_manager; match args { ThresholdArguments::Ecdsa(_) => { - ThresholdSignatureCycles::Ecdsa(cam.ecdsa_signature_fee(subnet_size, cost_schedule)) + ThresholdSignatureCycles::Ecdsa(cam.ecdsa_signature_fee(subnet_cycles_config)) + } + ThresholdArguments::Schnorr(_) => { + ThresholdSignatureCycles::Schnorr(cam.schnorr_signature_fee(subnet_cycles_config)) } - ThresholdArguments::Schnorr(_) => ThresholdSignatureCycles::Schnorr( - cam.schnorr_signature_fee(subnet_size, cost_schedule), - ), ThresholdArguments::VetKd(_) => { - ThresholdSignatureCycles::VetKd(cam.vetkd_fee(subnet_size, cost_schedule)) + ThresholdSignatureCycles::VetKd(cam.vetkd_fee(subnet_cycles_config)) } } } - #[allow(clippy::too_many_arguments)] fn sign_with_threshold( &self, mut request: Request, @@ -3740,7 +3663,6 @@ impl ExecutionEnvironment { derivation_path: Vec>, max_queue_size_registry: u32, state: &mut ReplicatedState, - subnet_size: usize, ) -> Result<(), UserError> { if let ThresholdArguments::Schnorr(schnorr) = &args { let alg = schnorr.key_id.algorithm; @@ -3767,8 +3689,8 @@ impl ExecutionEnvironment { let source_subnet = state.metadata.network_topology.route(request.sender.get()); let nns_subnet_id = state.metadata.network_topology.nns_subnet_id; if source_subnet != Some(nns_subnet_id) { - let cost_schedule = state.get_own_cost_schedule(); - let signature_fee = self.calculate_signature_fee(&args, subnet_size, cost_schedule); + let signature_fee = + self.calculate_signature_fee(&args, state.get_own_subnet_cycles_config()); let real_signature_fee = signature_fee.real(); if request.payment < real_signature_fee { return Err(UserError::new( @@ -3986,7 +3908,6 @@ impl ExecutionEnvironment { mut state: ReplicatedState, instruction_limits: InstructionLimits, round_limits: &mut RoundLimits, - subnet_size: usize, current_round: ExecutionRound, ) -> (ReplicatedState, ExecuteSubnetMessageResultType) { // Start logging execution time for `install_code`. @@ -4089,8 +4010,7 @@ impl ExecutionEnvironment { round_limits, compilation_cost_handling, round_counters, - subnet_size, - state.get_own_cost_schedule(), + state.get_own_subnet_cycles_config(), self.config.dirty_page_logging, ); self.process_install_code_result(state, dts_result, dts_status, since, current_round) @@ -4230,7 +4150,6 @@ impl ExecutionEnvironment { canister_id: &CanisterId, instruction_limits: InstructionLimits, round_limits: &mut RoundLimits, - subnet_size: usize, current_round: ExecutionRound, ) -> (ReplicatedState, ExecuteSubnetMessageResultType) { let task = state @@ -4294,7 +4213,6 @@ impl ExecutionEnvironment { state, instruction_limits, round_limits, - subnet_size, current_round, ), } @@ -4856,8 +4774,7 @@ fn execute_canister_input( time: Time, round_limits: &mut RoundLimits, resource_limits: ResourceLimits, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> ExecuteCanisterResult { let info = input.to_string(); let load_metrics = &mut canister @@ -4900,8 +4817,7 @@ fn execute_canister_input( network_topology, round_limits, resource_limits, - subnet_size, - cost_schedule, + subnet_cycles_config, ); let (canister, instructions_used, heap_delta, ingress_status) = exec_env.process_result(result); ExecuteCanisterResult { @@ -4924,8 +4840,7 @@ pub fn execute_canister( time: Time, round_limits: &mut RoundLimits, resource_limits: ResourceLimits, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> ExecuteCanisterResult { match canister.next_execution() { NextExecution::None | NextExecution::ContinueInstallCode => { @@ -4963,13 +4878,13 @@ pub fn execute_canister( counters: round_counters, log: &exec_env.log, time, - cost_schedule, + cost_schedule: subnet_cycles_config.cost_schedule, }; let result = paused.resume( canister, round_context, round_limits, - subnet_size, + subnet_cycles_config.subnet_size, &exec_env.call_tree_metrics, exec_env.deallocator_thread.sender(), ); @@ -5024,8 +4939,7 @@ pub fn execute_canister( time, round_limits, resource_limits, - subnet_size, - cost_schedule, + subnet_cycles_config, ) } diff --git a/rs/execution_environment/src/execution_environment/tests.rs b/rs/execution_environment/src/execution_environment/tests.rs index 8afe89e0e03a..abab616628f5 100644 --- a/rs/execution_environment/src/execution_environment/tests.rs +++ b/rs/execution_environment/src/execution_environment/tests.rs @@ -829,13 +829,13 @@ fn get_canister_status_from_another_canister_when_memory_low() { * seconds_per_day * test .cycles_account_manager() - .gib_storage_per_second_fee(test.subnet_size(), CanisterCyclesCostSchedule::Normal) + .gib_storage_per_second_fee(test.get_own_subnet_cycles_config()) .real() .get()) / one_gib + test .cycles_account_manager() - .base_per_second_fee(test.subnet_size(), CanisterCyclesCostSchedule::Normal) + .base_per_second_fee(test.get_own_subnet_cycles_config()) .real() .get() * seconds_per_day diff --git a/rs/execution_environment/src/execution_environment/tests/canister_snapshots.rs b/rs/execution_environment/src/execution_environment/tests/canister_snapshots.rs index 6b7ac9db939c..d0105082f57f 100644 --- a/rs/execution_environment/src/execution_environment/tests/canister_snapshots.rs +++ b/rs/execution_environment/src/execution_environment/tests/canister_snapshots.rs @@ -35,7 +35,7 @@ use ic_types::{ messages::{Payload, RejectContext, RequestOrResponse}, time::UNIX_EPOCH, }; -use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles}; +use ic_types_cycles::Cycles; use ic_types_test_utils::ids::user_test_id; use ic_universal_canister::{UNIVERSAL_CANISTER_WASM, wasm}; use more_asserts::{assert_gt, assert_lt}; @@ -1803,11 +1803,7 @@ fn take_canister_snapshot_charges_canister_cycles() { // Take a snapshot of the canister will decrease the balance. let expected_charge = test .cycles_account_manager() - .management_canister_cost( - instructions, - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, - ) + .management_canister_cost(instructions, test.get_own_subnet_cycles_config()) .real(); // Take a snapshot for the canister. @@ -1883,11 +1879,7 @@ fn load_canister_snapshot_charges_canister_cycles() { // Load a snapshot of the canister will decrease the balance. let expected_charge = test .cycles_account_manager() - .management_canister_cost( - instructions, - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, - ) + .management_canister_cost(instructions, test.get_own_subnet_cycles_config()) .real(); // Load an existing snapshot will decrease the balance. diff --git a/rs/execution_environment/src/execution_environment/tests/compilation.rs b/rs/execution_environment/src/execution_environment/tests/compilation.rs index 0b698e8daa62..ebe7c062dcae 100644 --- a/rs/execution_environment/src/execution_environment/tests/compilation.rs +++ b/rs/execution_environment/src/execution_environment/tests/compilation.rs @@ -9,7 +9,7 @@ mod execution_tests { use ic_test_utilities_execution_environment::{ExecutionTestBuilder, wat_compilation_cost}; use ic_test_utilities_metrics::{fetch_histogram_stats, fetch_int_counter_vec}; use ic_types::methods::WasmMethod; - use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles}; + use ic_types_cycles::Cycles; use ic_wasm_types::CanisterModule; use maplit::btreemap; use std::collections::BTreeSet; @@ -268,8 +268,7 @@ mod execution_tests { .cycles_account_manager() .execution_cost( DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + reduced_compilation_instructions, - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WasmExecutionMode::Wasm32 // Does not matter if it is Wasm64 or Wasm32 for this test. ) .real() @@ -309,8 +308,7 @@ mod execution_tests { .cycles_account_manager() .execution_cost( DEFAULT_CREATE_EXECUTION_STATE_BASE_COST + compilation_instructions, - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), WasmExecutionMode::Wasm32 // Does not matter if it is Wasm64 or Wasm32 for this test. ) .real() diff --git a/rs/execution_environment/src/hypervisor.rs b/rs/execution_environment/src/hypervisor.rs index 0980f53d9a16..e602a01ceca0 100644 --- a/rs/execution_environment/src/hypervisor.rs +++ b/rs/execution_environment/src/hypervisor.rs @@ -1,7 +1,7 @@ use ic_canister_sandbox_backend_lib::replica_controller::sandboxed_execution_controller::SandboxedExecutionController; use ic_config::execution_environment::{Config, MAX_COMPILATION_CACHE_SIZE}; use ic_config::flag_status::FlagStatus; -use ic_cycles_account_manager::CyclesAccountManager; +use ic_cycles_account_manager::{CyclesAccountManager, CyclesAccountManagerSubnetConfig}; use ic_embedders::{ CompilationCache, CompilationCacheBuilder, CompilationResult, WasmExecutionInput, WasmtimeEmbedder, @@ -25,7 +25,6 @@ use ic_types::{ CanisterId, DiskBytes, NumBytes, NumInstructions, SubnetId, Time, messages::RequestMetadata, methods::FuncRef, }; -use ic_types_cycles::CanisterCyclesCostSchedule; use ic_wasm_types::CanisterModule; use prometheus::{Histogram, IntCounter, IntGaugeVec}; use std::{path::Path, sync::Arc}; @@ -307,7 +306,7 @@ impl Hypervisor { state_changes_error: &IntCounter, call_tree_metrics: &dyn CallTreeMetrics, call_context_creation_time: Time, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> (WasmExecutionOutput, ExecutionState, SystemState) { assert_eq!( execution_parameters.instruction_limits.message(), @@ -325,7 +324,7 @@ impl Hypervisor { RequestMetadata::for_new_call_tree(time), round_limits, network_topology, - cost_schedule, + subnet_cycles_config, ); let (slice, mut output, canister_state_changes) = match execution_result { WasmExecutionResult::Finished(slice, output, canister_state_changes) => { @@ -370,7 +369,7 @@ impl Hypervisor { request_metadata: RequestMetadata, round_limits: &mut RoundLimits, network_topology: &NetworkTopology, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) -> WasmExecutionResult { assert_ge!( execution_parameters.instruction_limits.message(), @@ -400,7 +399,7 @@ impl Hypervisor { api_type.caller(), api_type.call_context_id(), execution_state.wasm_execution_mode.is_wasm64(), - cost_schedule, + subnet_cycles_config, ); let (compilation_result, mut execution_result) = Arc::clone(&self.wasm_executor).execute( WasmExecutionInput { diff --git a/rs/execution_environment/src/query_handler.rs b/rs/execution_environment/src/query_handler.rs index c781be5931c3..c7c09da86bad 100644 --- a/rs/execution_environment/src/query_handler.rs +++ b/rs/execution_environment/src/query_handler.rs @@ -280,8 +280,7 @@ impl InternalHttpQueryHandler { let response = self.canister_manager.get_canister_status( query.source(), canister, - state.get_ref().get_own_subnet_size(), - state.get_ref().get_own_cost_schedule(), + state.get_ref().get_own_subnet_cycles_config(), ready_for_migration, state.get_ref().get_own_subnet_admins(), )?; diff --git a/rs/execution_environment/src/query_handler/query_context.rs b/rs/execution_environment/src/query_handler/query_context.rs index cd37d1075448..40fed5e44d4c 100644 --- a/rs/execution_environment/src/query_handler/query_context.rs +++ b/rs/execution_environment/src/query_handler/query_context.rs @@ -13,7 +13,9 @@ use crate::{ }; use ic_base_types::NumBytes; use ic_config::flag_status::FlagStatus; -use ic_cycles_account_manager::{CyclesAccountManager, ResourceSaturation}; +use ic_cycles_account_manager::{ + CyclesAccountManager, CyclesAccountManagerSubnetConfig, ResourceSaturation, +}; use ic_embedders::wasmtime_embedder::system_api::{ ApiType, ExecutionParameters, InstructionLimits, }; @@ -23,7 +25,6 @@ use ic_interfaces::execution_environment::{ SystemApiCallCounters, }; use ic_interfaces_state_manager::Labeled; -use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_logger::{ReplicaLogger, error, info}; use ic_query_stats::QueryStatsCollector; use ic_registry_subnet_type::SubnetType; @@ -40,7 +41,7 @@ use ic_types::{ }, methods::{FuncRef, WasmClosure, WasmMethod}, }; -use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles}; +use ic_types_cycles::Cycles; use prometheus::IntCounter; use std::{ collections::{BTreeMap, VecDeque}, @@ -396,11 +397,6 @@ impl<'a> QueryContext<'a> { )), ); } - let cost_schedule = self.get_cost_schedule(); - let subnet_size = self - .network_topology - .get_subnet_size(&self.cycles_account_manager.get_subnet_id()) - .unwrap_or(SMALL_APP_SUBNET_MAX_SIZE); if self .cycles_account_manager .can_withdraw_cycles_with_threshold( @@ -409,8 +405,7 @@ impl<'a> QueryContext<'a> { canister.memory_usage(), canister.message_memory_usage(), canister.system_state.reserved_balance(), - subnet_size, - self.get_cost_schedule(), + self.get_own_subnet_cycles_config(), false, ) .is_err() @@ -434,6 +429,7 @@ impl<'a> QueryContext<'a> { let execution_parameters = self.execution_parameters(&canister, instruction_limits); let data_certificate = self.get_data_certificate(&canister.canister_id()); + let own_subnet_cycles_config = self.get_own_subnet_cycles_config(); let (mut canister, instructions_left, result, call_context_id, system_api_call_counters) = execute_non_replicated_query( query_kind, @@ -447,7 +443,7 @@ impl<'a> QueryContext<'a> { self.hypervisor, &mut self.round_limits, self.query_critical_error, - cost_schedule, + own_subnet_cycles_config, ); self.add_system_api_call_counters(system_api_call_counters); let instructions_executed = instruction_limit - instructions_left; @@ -651,8 +647,7 @@ impl<'a> QueryContext<'a> { ), }; - let cost_schedule = self.get_cost_schedule(); - + let own_subnet_cycles_config = self.get_own_subnet_cycles_config(); let (output, output_execution_state, output_system_state) = self.hypervisor.execute( api_type, time, @@ -667,7 +662,7 @@ impl<'a> QueryContext<'a> { self.query_critical_error, &CallTreeMetricsNoOp, call_context.time(), - cost_schedule, + own_subnet_cycles_config, ); self.add_system_api_call_counters(output.system_api_call_counters); @@ -748,7 +743,7 @@ impl<'a> QueryContext<'a> { FuncRef::QueryClosure(cleanup_closure) } }; - let cost_schedule = self.get_cost_schedule(); + let own_subnet_cycles_config = self.get_own_subnet_cycles_config(); let (cleanup_output, output_execution_state, output_system_state) = self.hypervisor.execute( ApiType::CompositeCleanup { @@ -769,7 +764,7 @@ impl<'a> QueryContext<'a> { self.query_critical_error, &CallTreeMetricsNoOp, time, - cost_schedule, + own_subnet_cycles_config, ); self.add_system_api_call_counters(cleanup_output.system_api_call_counters); @@ -1124,7 +1119,7 @@ impl<'a> QueryContext<'a> { self.transient_errors } - pub fn get_cost_schedule(&self) -> CanisterCyclesCostSchedule { - self.state.get_ref().get_own_cost_schedule() + fn get_own_subnet_cycles_config(&self) -> CyclesAccountManagerSubnetConfig { + self.state.get_ref().get_own_subnet_cycles_config() } } diff --git a/rs/execution_environment/src/scheduler.rs b/rs/execution_environment/src/scheduler.rs index 3eda6c33c653..0a684fbf81ae 100644 --- a/rs/execution_environment/src/scheduler.rs +++ b/rs/execution_environment/src/scheduler.rs @@ -16,7 +16,7 @@ use ic_config::embedders::Config as HypervisorConfig; use ic_config::flag_status::FlagStatus; use ic_config::subnet_config::SchedulerConfig; use ic_crypto_prng::{Csprng, RandomnessPurpose::ExecutionThread}; -use ic_cycles_account_manager::CyclesAccountManager; +use ic_cycles_account_manager::{CyclesAccountManager, CyclesAccountManagerSubnetConfig}; use ic_embedders::wasmtime_embedder::system_api::InstructionLimits; use ic_error_types::{ErrorCode, UserError}; use ic_interfaces::execution_environment::{ @@ -199,7 +199,6 @@ impl SchedulerImpl { round_limits: &mut RoundLimits, long_running_canisters: &[CanisterId], measurement_scope: &MeasurementScope, - subnet_size: usize, current_round: ExecutionRound, ) -> ReplicatedState { let mut ongoing_long_install_code = false; @@ -221,7 +220,6 @@ impl SchedulerImpl { canister_id, instruction_limits, round_limits, - subnet_size, current_round, ); state = new_state; @@ -363,8 +361,7 @@ impl SchedulerImpl { ) -> BTreeSet { let mut heartbeat_and_timer_canisters = BTreeSet::new(); let now = state.time(); - let cost_schedule = state.get_own_cost_schedule(); - let subnet_size = state.get_own_subnet_size(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); for canister in state.hot_canisters_iter_mut() { // Add `Heartbeat` or `GlobalTimer` for running canisters only. @@ -392,8 +389,7 @@ impl SchedulerImpl { .can_prepay_execution_cycles( canister, self.config.max_instructions_per_message, - subnet_size, - cost_schedule, + subnet_cycles_config, ) .is_err() { @@ -429,7 +425,7 @@ impl SchedulerImpl { root_measurement_scope: &MeasurementScope<'a>, round_log: &ReplicaLogger, ) -> ReplicatedState { - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let measurement_scope = MeasurementScope::nested(&self.metrics.round_inner, root_measurement_scope); @@ -530,8 +526,7 @@ impl SchedulerImpl { current_round, state.time(), Arc::new(state.metadata.network_topology.clone()), - registry_settings.subnet_size, - cost_schedule, + subnet_cycles_config, &mut round_limits, state.resource_limits(), &measurement_scope, @@ -644,8 +639,7 @@ impl SchedulerImpl { round_id: ExecutionRound, time: Time, network_topology: Arc, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, round_limits: &mut RoundLimits, resource_limits: ResourceLimits, measurement_scope: &MeasurementScope, @@ -699,8 +693,7 @@ impl SchedulerImpl { time, network_topology, rate_limiting_of_heap_delta, - subnet_size, - cost_schedule, + subnet_cycles_config, round_limits, resource_limits, metrics, @@ -826,7 +819,6 @@ impl SchedulerImpl { fn charge_canisters_for_resource_allocation_and_usage( &self, state: &mut ReplicatedState, - subnet_size: usize, current_round: ExecutionRound, current_round_type: ExecutionRoundType, ) { @@ -844,6 +836,7 @@ impl SchedulerImpl { } let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let state_time = state.time(); let threshold_last_allocation_charge = state_time.saturating_sub( self.cycles_account_manager @@ -877,8 +870,7 @@ impl SchedulerImpl { &self.log, canister, duration_since_last_charge, - subnet_size, - cost_schedule, + subnet_cycles_config, ) .is_err() { @@ -1424,7 +1416,6 @@ impl Scheduler for SchedulerImpl { &mut subnet_round_limits, &long_running_canisters, &measurement_scope, - registry_settings.subnet_size, current_round, ); @@ -1543,7 +1534,6 @@ impl Scheduler for SchedulerImpl { let _timer = self.metrics.round_finalization_charge.start_timer(); self.charge_canisters_for_resource_allocation_and_usage( &mut final_state, - registry_settings.subnet_size, current_round, current_round_type, ); @@ -1637,8 +1627,7 @@ fn execute_canisters_on_thread( time: Time, network_topology: Arc, rate_limiting_of_heap_delta: FlagStatus, - subnet_size: usize, - cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, mut round_limits: RoundLimits, resource_limits: ResourceLimits, metrics: Arc, @@ -1715,8 +1704,7 @@ fn execute_canisters_on_thread( time, &mut round_limits, resource_limits, - subnet_size, - cost_schedule, + subnet_cycles_config, ); ingress_results.extend(ingress_status); let round_instructions_executed = diff --git a/rs/execution_environment/src/scheduler/test_utilities.rs b/rs/execution_environment/src/scheduler/test_utilities.rs index 7d6cee133472..def6fd405318 100644 --- a/rs/execution_environment/src/scheduler/test_utilities.rs +++ b/rs/execution_environment/src/scheduler/test_utilities.rs @@ -10,7 +10,7 @@ use ic_config::{ flag_status::FlagStatus, subnet_config::{SchedulerConfig, SubnetConfig, SubnetSecurity}, }; -use ic_cycles_account_manager::CyclesAccountManager; +use ic_cycles_account_manager::{CyclesAccountManager, CyclesAccountManagerSubnetConfig}; use ic_embedders::{ CompilationCache, CompilationResult, WasmExecutionInput, wasm_executor::{ @@ -256,8 +256,7 @@ impl SchedulerTest { .cycles_account_manager .execution_cost( num_instructions, - self.subnet_size(), - self.state.as_ref().unwrap().get_own_cost_schedule(), + self.get_own_subnet_cycles_config(), WasmExecutionMode::Wasm32, ) .real() @@ -666,11 +665,9 @@ impl SchedulerTest { } pub fn charge_for_resource_allocations(&mut self) { - let subnet_size = self.subnet_size(); self.scheduler .charge_canisters_for_resource_allocation_and_usage( self.state.as_mut().unwrap(), - subnet_size, ExecutionRound::from(0), ExecutionRoundType::CheckpointRound, ) @@ -707,24 +704,20 @@ impl SchedulerTest { self.state_mut().metadata.batch_time += duration; } - pub fn subnet_size(&self) -> usize { - self.registry_settings.subnet_size + pub(crate) fn get_own_subnet_cycles_config(&self) -> CyclesAccountManagerSubnetConfig { + self.state().get_own_subnet_cycles_config() } pub fn ecdsa_signature_fee(&self) -> CompoundCycles { - self.scheduler.cycles_account_manager.ecdsa_signature_fee( - self.registry_settings.subnet_size, - self.state().get_own_cost_schedule(), - ) + self.scheduler + .cycles_account_manager + .ecdsa_signature_fee(self.get_own_subnet_cycles_config()) } pub fn schnorr_signature_fee(&self) -> Cycles { self.scheduler .cycles_account_manager - .schnorr_signature_fee( - self.registry_settings.subnet_size, - self.state().get_own_cost_schedule(), - ) + .schnorr_signature_fee(self.get_own_subnet_cycles_config()) .real() } @@ -736,8 +729,7 @@ impl SchedulerTest { self.scheduler.cycles_account_manager.http_request_fee( request_size, response_size_limit, - self.subnet_size(), - self.state.as_ref().unwrap().get_own_cost_schedule(), + self.get_own_subnet_cycles_config(), ) } @@ -749,8 +741,7 @@ impl SchedulerTest { self.scheduler.cycles_account_manager.memory_cost( bytes, duration, - self.subnet_size(), - self.state.as_ref().unwrap().get_own_cost_schedule(), + self.get_own_subnet_cycles_config(), ) } @@ -762,8 +753,7 @@ impl SchedulerTest { self.scheduler.cycles_account_manager.canister_base_cost( bytes, duration, - self.subnet_size(), - self.state.as_ref().unwrap().get_own_cost_schedule(), + self.get_own_subnet_cycles_config(), ) } @@ -777,8 +767,7 @@ impl SchedulerTest { .compute_allocation_cost( compute_allocation, duration, - self.subnet_size(), - self.state.as_ref().unwrap().get_own_cost_schedule(), + self.get_own_subnet_cycles_config(), ) } @@ -1518,24 +1507,22 @@ impl TestWasmExecutorCore { let call_message_id = self.next_message_id(); let response_message_id = self.next_message_id(); let closure = WasmClosure::new(0, response_message_id.into()); + let subnet_cycles_config = + CyclesAccountManagerSubnetConfig::new(self.subnet_size, system_state.cost_schedule()); let prepayment_for_response_execution = self .cycles_account_manager .prepayment_for_response_execution( - self.subnet_size, - system_state.cost_schedule(), + subnet_cycles_config, WasmExecutionMode::from_is_wasm64(system_state.is_wasm64_execution), ); let prepayment_for_response_transmission = self .cycles_account_manager - .prepayment_for_response_transmission(self.subnet_size, system_state.cost_schedule()); + .prepayment_for_response_transmission(subnet_cycles_config); // Scheduler uses `TestCall` requests which have zero payload. let payload_size = NumBytes::from(0); - let prepayment_for_call_transmission = - self.cycles_account_manager.xnet_total_transmission_fee( - payload_size, - self.subnet_size, - system_state.cost_schedule(), - ); + let prepayment_for_call_transmission = self + .cycles_account_manager + .xnet_total_transmission_fee(payload_size, subnet_cycles_config); let deadline = NO_DEADLINE; let request = OutputRequest { receiver, diff --git a/rs/execution_environment/tests/hypervisor.rs b/rs/execution_environment/tests/hypervisor.rs index c3b76707253e..28beba7f89c8 100644 --- a/rs/execution_environment/tests/hypervisor.rs +++ b/rs/execution_environment/tests/hypervisor.rs @@ -49,7 +49,7 @@ use ic_types::{ ingress::{IngressState, IngressStatus, WasmResult}, methods::WasmMethod, }; -use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles}; +use ic_types_cycles::Cycles; use ic_universal_canister::{CallArgs, UNIVERSAL_CANISTER_WASM, call_args, wasm}; use more_asserts::{assert_ge, assert_gt, assert_le, assert_lt}; #[cfg(not(all(target_arch = "aarch64", target_vendor = "apple")))] @@ -3540,27 +3540,24 @@ fn ic0_call_cycles_add_deducts_cycles() { assert_eq!(1, test.xnet_messages().len()); let mgr = test.cycles_account_manager(); let messaging_fee = mgr - .xnet_call_performed_fee(test.subnet_size(), CanisterCyclesCostSchedule::Normal) + .xnet_call_performed_fee(test.get_own_subnet_cycles_config()) .real() + mgr .xnet_call_bytes_transmitted_fee( test.xnet_messages()[0].payload_size_bytes(), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), ) .real() + mgr .xnet_call_bytes_transmitted_fee( MAX_INTER_CANISTER_PAYLOAD_IN_BYTES, - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), ) .real() + mgr .execution_cost( MAX_NUM_INSTRUCTIONS, - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(canister_id), ) .real(); @@ -6837,8 +6834,7 @@ fn dts_abort_works_in_update_call() { .cycles_account_manager() .execution_cost( NumInstructions::from(100_000_000), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(canister_id) ) .real(), @@ -6874,8 +6870,7 @@ fn dts_abort_works_in_update_call() { .cycles_account_manager() .execution_cost( NumInstructions::from(100_000_000), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), test.canister_wasm_execution_mode(canister_id) ) .real(), @@ -8382,8 +8377,7 @@ fn memory_grow_succeeds_in_post_upgrade_if_the_same_amount_is_dropped_after_pre_ NumBytes::new(memory_usage), MessageMemoryUsage::ZERO, ComputeAllocation::zero(), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), Cycles::zero(), ); @@ -8472,8 +8466,7 @@ fn set_reserved_cycles_limit_below_existing_fails() { .storage_reservation_cycles( memory_usage_after - memory_usage_before, &ResourceSaturation::new(subnet_memory_usage, THRESHOLD, CAPACITY), - test.subnet_size(), - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), ) .real() ); @@ -9322,9 +9315,8 @@ fn invoke_cost_call() { let res = test.ingress(canister_id, "update", payload); let expected_cost = test.cycles_account_manager().xnet_call_total_fee( (method_name.len() as u64 + argument.len() as u64).into(), - test.subnet_size(), + test.get_own_subnet_cycles_config(), WasmExecutionMode::Wasm32, - CanisterCyclesCostSchedule::Normal, ); let Ok(WasmResult::Reply(bytes)) = res else { panic!("Expected reply, got {res:?}"); @@ -9336,7 +9328,6 @@ fn invoke_cost_call() { #[test] fn invoke_cost_create_canister() { let mut test = ExecutionTestBuilder::new().build(); - let subnet_size = test.subnet_size(); let canister_id = test.universal_canister().unwrap(); let payload = wasm() .cost_create_canister() @@ -9346,7 +9337,7 @@ fn invoke_cost_create_canister() { let res = test.ingress(canister_id, "update", payload); let expected_cost = test .cycles_account_manager() - .canister_creation_fee(subnet_size, CanisterCyclesCostSchedule::Normal); + .canister_creation_fee(test.get_own_subnet_cycles_config()); let Ok(WasmResult::Reply(bytes)) = res else { panic!("Expected reply, got {res:?}"); }; @@ -9357,7 +9348,6 @@ fn invoke_cost_create_canister() { #[test] fn invoke_cost_http_request() { let mut test = ExecutionTestBuilder::new().build(); - let subnet_size = test.subnet_size(); let canister_id = test.universal_canister().unwrap(); let request_size = 1000; let max_res_bytes = 1_800_000; @@ -9370,8 +9360,7 @@ fn invoke_cost_http_request() { let expected_cost = test.cycles_account_manager().http_request_fee( request_size.into(), Some(max_res_bytes.into()), - subnet_size, - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), ); let Ok(WasmResult::Reply(bytes)) = res else { panic!("Expected reply, got {res:?}"); @@ -9391,7 +9380,6 @@ fn invoke_cost_http_request_v2() { transform_instructions: u64, } let mut test = ExecutionTestBuilder::new().build(); - let subnet_size = test.subnet_size(); let canister_id = test.universal_canister().unwrap(); let request_bytes = 1000; let http_roundtrip_time_ms = 2_000; @@ -9419,8 +9407,7 @@ fn invoke_cost_http_request_v2() { raw_response_bytes.into(), transform_instructions.into(), transformed_response_bytes.into(), - subnet_size, - CanisterCyclesCostSchedule::Normal, + test.get_own_subnet_cycles_config(), ); let bytes = get_reply(res); let actual_cost = Cycles::from(&bytes); @@ -9478,7 +9465,6 @@ fn invoke_cost_sign_with_ecdsa() { name: key_name.clone(), })) .build(); - let subnet_size = test.subnet_size(); let canister_id = test.universal_canister().unwrap(); let payload = wasm() .cost_sign_with_ecdsa(key_name.as_bytes(), curve_variant) @@ -9488,7 +9474,7 @@ fn invoke_cost_sign_with_ecdsa() { let res = test.ingress(canister_id, "update", payload); let expected_cost = test .cycles_account_manager() - .ecdsa_signature_fee(subnet_size, CanisterCyclesCostSchedule::Normal); + .ecdsa_signature_fee(test.get_own_subnet_cycles_config()); let Ok(WasmResult::Reply(bytes)) = res else { panic!("Expected reply, got {res:?}"); }; @@ -9558,7 +9544,6 @@ fn invoke_cost_sign_with_schnorr() { name: key_name.clone(), })) .build(); - let subnet_size = test.subnet_size(); let canister_id = test.universal_canister().unwrap(); let payload = wasm() .cost_sign_with_schnorr(key_name.as_bytes(), algorithm_variant) @@ -9568,7 +9553,7 @@ fn invoke_cost_sign_with_schnorr() { let res = test.ingress(canister_id, "update", payload); let expected_cost = test .cycles_account_manager() - .schnorr_signature_fee(subnet_size, CanisterCyclesCostSchedule::Normal); + .schnorr_signature_fee(test.get_own_subnet_cycles_config()); let Ok(WasmResult::Reply(bytes)) = res else { panic!("Expected reply, got {res:?}"); }; @@ -9638,7 +9623,6 @@ fn invoke_cost_vetkd_derive_key() { name: key_name.clone(), })) .build(); - let subnet_size = test.subnet_size(); let canister_id = test.universal_canister().unwrap(); let payload = wasm() .cost_vetkd_derive_key(key_name.as_bytes(), curve_variant) @@ -9648,7 +9632,7 @@ fn invoke_cost_vetkd_derive_key() { let res = test.ingress(canister_id, "update", payload); let expected_cost = test .cycles_account_manager() - .vetkd_fee(subnet_size, CanisterCyclesCostSchedule::Normal); + .vetkd_fee(test.get_own_subnet_cycles_config()); let Ok(WasmResult::Reply(bytes)) = res else { panic!("Expected reply, got {res:?}"); }; diff --git a/rs/ingress_manager/src/ingress_selector.rs b/rs/ingress_manager/src/ingress_selector.rs index 6fda65cc1109..f5ad33e003e9 100644 --- a/rs/ingress_manager/src/ingress_selector.rs +++ b/rs/ingress_manager/src/ingress_selector.rs @@ -552,12 +552,11 @@ impl IngressManager { let effective_canister_id = extract_effective_canister_id(msg).map_err(|_| { ValidationError::InvalidArtifact(InvalidIngressPayloadReason::InvalidManagementMessage) })?; - let subnet_size = state.get_own_subnet_size(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); match self.cycles_account_manager.ingress_induction_cost( signed_ingress, effective_canister_id, - subnet_size, - state.get_own_cost_schedule(), + subnet_cycles_config, ) { IngressInductionCost::Fee { payer, @@ -574,8 +573,7 @@ impl IngressManager { canister.memory_usage(), canister.message_memory_usage(), canister.system_state.reserved_balance(), - subnet_size, - state.get_own_cost_schedule(), + subnet_cycles_config, false, // error here is not returned back to the user => no need to reveal top up balance ) { @@ -769,6 +767,7 @@ pub(crate) mod tests { use assert_matches::assert_matches; use ic_artifact_pool::ingress_pool::IngressPoolImpl; use ic_crypto_temp_crypto::temp_crypto_component_with_fake_registry; + use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_interfaces::{ execution_environment::IngressHistoryError, ingress_pool::ChangeAction, @@ -1612,8 +1611,10 @@ pub(crate) mod tests { .ingress_induction_cost( &m1, None, - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ) .cost(), ) diff --git a/rs/messaging/src/scheduling/valid_set_rule.rs b/rs/messaging/src/scheduling/valid_set_rule.rs index fd053a64ce3d..d5941760d0cc 100644 --- a/rs/messaging/src/scheduling/valid_set_rule.rs +++ b/rs/messaging/src/scheduling/valid_set_rule.rs @@ -147,7 +147,6 @@ impl> &self, state: &mut ReplicatedState, msg: SignedIngress, - subnet_size: usize, current_round: ExecutionRound, ) { trace!(self.log, "induct_message"); @@ -159,7 +158,7 @@ impl> let time = state.time(); let ingress_expiry = ingress_content.ingress_expiry(); - let status = match self.enqueue(state, msg, subnet_size) { + let status = match self.enqueue(state, msg) { Ok(()) => { self.observe_inducted_ingress_payload_size(payload_bytes); self.ingress_history_writer.set_status( @@ -247,7 +246,6 @@ impl> &self, state: &mut ReplicatedState, signed_ingress: SignedIngress, - subnet_size: usize, ) -> Result<(), IngressInductionError> { if state.metadata.own_subnet_type != SubnetType::System && state.metadata.ingress_history.len() >= self.ingress_history_max_messages @@ -277,12 +275,11 @@ impl> }; // Compute the cost of induction. - let cost_schedule = state.get_own_cost_schedule(); + let subnet_cycles_config = state.get_own_subnet_cycles_config(); let induction_cost = self.cycles_account_manager.ingress_induction_cost( &signed_ingress, effective_canister_id, - subnet_size, - cost_schedule, + subnet_cycles_config, ); let ingress = Ingress::from((signed_ingress.take_content(), effective_canister_id)); @@ -311,8 +308,7 @@ impl> message_memory_usage, compute_allocation, cost, - subnet_size, - cost_schedule, + subnet_cycles_config, reveal_top_up, ) { return Err(IngressInductionError::CanisterOutOfCycles(err)); @@ -350,11 +346,10 @@ impl> Valid msgs: Vec, current_round: ExecutionRound, ) { - let subnet_size = state.get_own_subnet_size(); for msg in msgs { let message_id = msg.content().id(); if !self.is_duplicate(state, &msg) { - self.induct_message(state, msg, subnet_size, current_round); + self.induct_message(state, msg, current_round); } else { self.observe_inducted_ingress_status(LABEL_VALUE_DUPLICATE); debug!(self.log, "Didn't induct duplicate message {}", message_id); diff --git a/rs/messaging/src/scheduling/valid_set_rule/test.rs b/rs/messaging/src/scheduling/valid_set_rule/test.rs index 079fda7cc27f..2f63592aa459 100644 --- a/rs/messaging/src/scheduling/valid_set_rule/test.rs +++ b/rs/messaging/src/scheduling/valid_set_rule/test.rs @@ -28,7 +28,6 @@ use ic_types::{ messages::{MessageId, SignedIngress}, time::UNIX_EPOCH, }; -use ic_types_cycles::CanisterCyclesCostSchedule; use mockall::predicate::{always, eq}; struct NoopIngressHistoryWriter; @@ -136,12 +135,7 @@ fn induct_message_with_successful_history_update() { let mut state = ReplicatedState::new(subnet_test_id(1), subnet_type); insert_canister(&mut state, canister_id); - valid_set_rule.induct_message( - &mut state, - signed_ingress, - SMALL_APP_SUBNET_MAX_SIZE, - ExecutionRound::from(0), - ); + valid_set_rule.induct_message(&mut state, signed_ingress, ExecutionRound::from(0)); assert_eq!(ingress_queue_size(&state, canister_id), 1); assert_inducted_ingress_messages_eq( metric_vec(&[(&[(LABEL_STATUS, LABEL_VALUE_SUCCESS)], 1)]), @@ -201,12 +195,7 @@ fn induct_message_fails_for_stopping_canister() { let mut state = ReplicatedState::new(subnet_test_id(1), SubnetType::Application); state.put_canister_state(get_stopping_canister(canister_id)); - valid_set_rule.induct_message( - &mut state, - signed_ingress, - SMALL_APP_SUBNET_MAX_SIZE, - ExecutionRound::from(0), - ); + valid_set_rule.induct_message(&mut state, signed_ingress, ExecutionRound::from(0)); assert_eq!(ingress_queue_size(&state, canister_id), 0); assert_inducted_ingress_messages_eq( metric_vec(&[(&[(LABEL_STATUS, LABEL_VALUE_CANISTER_STOPPING)], 1)]), @@ -264,12 +253,7 @@ fn induct_message_fails_for_stopped_canister() { let mut state = ReplicatedState::new(subnet_test_id(1), SubnetType::Application); state.put_canister_state(get_stopped_canister(canister_id)); - valid_set_rule.induct_message( - &mut state, - signed_ingress, - SMALL_APP_SUBNET_MAX_SIZE, - ExecutionRound::from(0), - ); + valid_set_rule.induct_message(&mut state, signed_ingress, ExecutionRound::from(0)); assert_eq!(ingress_queue_size(&state, canister_id), 0); assert_inducted_ingress_messages_eq( metric_vec(&[(&[(LABEL_STATUS, LABEL_VALUE_CANISTER_STOPPED)], 1)]), @@ -315,12 +299,7 @@ fn try_to_induct_a_message_marked_as_already_inducted() { state: IngressState::Received, }; state.set_ingress_status(msg.id(), status, NumBytes::from(u64::MAX), |_| {}); - valid_set_rule.induct_message( - &mut state, - signed_ingress, - SMALL_APP_SUBNET_MAX_SIZE, - ExecutionRound::from(0), - ); + valid_set_rule.induct_message(&mut state, signed_ingress, ExecutionRound::from(0)); }); } @@ -370,12 +349,7 @@ fn update_history_if_induction_failed() { let mut state = ReplicatedState::new(subnet_test_id(1), SubnetType::Application); // The induction is expected to fail because there is no canister 0 in the // ReplicatedState. - valid_set_rule.induct_message( - &mut state, - signed_ingress.clone(), - SMALL_APP_SUBNET_MAX_SIZE, - ExecutionRound::from(0), - ); + valid_set_rule.induct_message(&mut state, signed_ingress.clone(), ExecutionRound::from(0)); assert!(state.canister_state(&canister_id).is_none()); assert_eq!(state.get_ingress_status(&msg.id()), &status_clone); assert_inducted_ingress_messages_eq( @@ -513,12 +487,7 @@ fn canister_on_application_subnet_charges_for_ingress() { .canister_id(canister_test_id(0)) .build(); let cost_of_ingress = cycles_account_manager - .ingress_induction_cost( - &signed_ingress, - None, - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, - ) + .ingress_induction_cost(&signed_ingress, None, state.get_own_subnet_cycles_config()) .cost(); let ingress_history_writer = NoopIngressHistoryWriter; @@ -619,7 +588,7 @@ fn ingress_to_stopping_canister_is_rejected() { .canister_id(canister_test_id(0)) .build(); assert_eq!( - valid_set_rule.enqueue(&mut state, ingress, SMALL_APP_SUBNET_MAX_SIZE), + valid_set_rule.enqueue(&mut state, ingress), Err(IngressInductionError::CanisterStopping(canister_test_id(0))) ); } @@ -649,7 +618,7 @@ fn ingress_to_stopped_canister_is_rejected() { .build(); assert_eq!( - valid_set_rule.enqueue(&mut state, ingress, SMALL_APP_SUBNET_MAX_SIZE), + valid_set_rule.enqueue(&mut state, ingress), Err(IngressInductionError::CanisterStopped(canister_test_id(0))) ); } @@ -684,17 +653,11 @@ fn running_canister_on_application_subnet_accepts_and_charges_for_ingress() { .ingress_induction_cost( &ingress, effective_canister_id, - SMALL_APP_SUBNET_MAX_SIZE, - CanisterCyclesCostSchedule::Normal, + state.get_own_subnet_cycles_config(), ) .cost(); - valid_set_rule.induct_message( - &mut state, - ingress, - SMALL_APP_SUBNET_MAX_SIZE, - ExecutionRound::from(0), - ); + valid_set_rule.induct_message(&mut state, ingress, ExecutionRound::from(0)); let balance_after = state .canister_state(&canister_id) @@ -733,12 +696,7 @@ fn running_canister_on_system_subnet_accepts_and_does_not_charge_for_ingress() { state.put_canister_state(canister); let ingress = SignedIngressBuilder::new().build(); - valid_set_rule.induct_message( - &mut state, - ingress, - SMALL_APP_SUBNET_MAX_SIZE, - ExecutionRound::from(0), - ); + valid_set_rule.induct_message(&mut state, ingress, ExecutionRound::from(0)); let balance_after = state .canister_state(&canister_id) @@ -772,7 +730,7 @@ fn management_message_with_unknown_method_is_not_inducted() { .method_name("test") .build(); assert_eq!( - valid_set_rule.enqueue(&mut state, ingress, SMALL_APP_SUBNET_MAX_SIZE), + valid_set_rule.enqueue(&mut state, ingress), Err(IngressInductionError::CanisterMethodNotFound(String::from( "test" ))) @@ -803,7 +761,7 @@ fn management_message_with_invalid_payload_is_not_inducted() { .build(); assert_eq!( - valid_set_rule.enqueue(&mut state, ingress, SMALL_APP_SUBNET_MAX_SIZE), + valid_set_rule.enqueue(&mut state, ingress), Err(IngressInductionError::InvalidManagementPayload) ); } @@ -843,11 +801,7 @@ fn management_message_update_setting_is_inducted_but_not_charged() { .method_name("update_settings") .method_payload(payload) .build(); - assert!( - valid_set_rule - .enqueue(&mut state, ingress, SMALL_APP_SUBNET_MAX_SIZE) - .is_ok() - ); + assert!(valid_set_rule.enqueue(&mut state, ingress).is_ok()); let balance_after = state .canister_state(&canister_id) diff --git a/rs/replicated_state/src/replicated_state.rs b/rs/replicated_state/src/replicated_state.rs index 6c3598a62548..ca5c13d0cfae 100644 --- a/rs/replicated_state/src/replicated_state.rs +++ b/rs/replicated_state/src/replicated_state.rs @@ -36,7 +36,8 @@ use ic_types::{ time::CoarseTime, }; use ic_types_cycles::{ - CanisterCyclesCostSchedule, CompoundCycles, CyclesUseCaseKind, DroppedMessages, + CanisterCyclesCostSchedule, CompoundCycles, CyclesAccountManagerSubnetConfig, + CyclesUseCaseKind, DroppedMessages, }; use ic_validate_eq::ValidateEq; use ic_validate_eq_derive::ValidateEq; @@ -769,6 +770,14 @@ impl ReplicatedState { self.metadata.own_cost_schedule().unwrap_or_default() } + /// Returns the cycles account manager subnet config for this subnet. + pub fn get_own_subnet_cycles_config(&self) -> CyclesAccountManagerSubnetConfig { + CyclesAccountManagerSubnetConfig::new( + self.get_own_subnet_size(), + self.get_own_cost_schedule(), + ) + } + /// Returns the list of subnet admins of this subnet. pub fn get_own_subnet_admins(&self) -> Option> { let subnet_id = self.metadata.own_subnet_id; diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 29187c2ff0f6..61a57abda8a7 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -1254,7 +1254,6 @@ pub struct StateMachine { chain_key_payload_builder: Arc, remove_old_states: bool, cycles_account_manager: Arc, - cost_schedule: CanisterCyclesCostSchedule, hypervisor_config: HypervisorConfig, } @@ -2442,7 +2441,6 @@ impl StateMachine { chain_key_payload_builder, remove_old_states, cycles_account_manager: execution_services.cycles_account_manager, - cost_schedule, hypervisor_config, } } @@ -4677,12 +4675,10 @@ impl StateMachine { ) -> IngressInductionCost { let msg = self.ingress_message(sender, canister_id, method, payload, None); let effective_canister_id = extract_effective_canister_id(msg.content()).unwrap(); - let subnet_size = self.nodes.len(); self.cycles_account_manager.ingress_induction_cost( &msg, effective_canister_id, - subnet_size, - self.cost_schedule, + self.get_latest_state().get_own_subnet_cycles_config(), ) } diff --git a/rs/test_utilities/embedders/BUILD.bazel b/rs/test_utilities/embedders/BUILD.bazel index 3901248e83b9..3576d1300fbc 100644 --- a/rs/test_utilities/embedders/BUILD.bazel +++ b/rs/test_utilities/embedders/BUILD.bazel @@ -14,6 +14,7 @@ rust_library( "//rs/cycles_account_manager", "//rs/embedders", "//rs/interfaces", + "//rs/limits", "//rs/monitoring/logger", "//rs/registry/subnet_type", "//rs/replicated_state", diff --git a/rs/test_utilities/embedders/Cargo.toml b/rs/test_utilities/embedders/Cargo.toml index 479427112e82..fc460843db69 100644 --- a/rs/test_utilities/embedders/Cargo.toml +++ b/rs/test_utilities/embedders/Cargo.toml @@ -12,6 +12,7 @@ ic-config = { path = "../../config" } ic-cycles-account-manager = { path = "../../cycles_account_manager" } ic-embedders = { path = "../../embedders" } ic-interfaces = { path = "../../interfaces" } +ic-limits = { path = "../../limits" } ic-logger = { path = "../../monitoring/logger" } ic-management-canister-types-private = { path = "../../types/management_canister_types" } ic-registry-subnet-type = { path = "../../registry/subnet_type" } diff --git a/rs/test_utilities/embedders/src/lib.rs b/rs/test_utilities/embedders/src/lib.rs index 5866c29f4af6..07eaf86bc7b9 100644 --- a/rs/test_utilities/embedders/src/lib.rs +++ b/rs/test_utilities/embedders/src/lib.rs @@ -5,7 +5,7 @@ use ic_base_types::NumBytes; use ic_config::execution_environment::Config as HypervisorConfig; use ic_config::flag_status::FlagStatus; use ic_config::subnet_config::SchedulerConfig; -use ic_cycles_account_manager::ResourceSaturation; +use ic_cycles_account_manager::{CyclesAccountManagerSubnetConfig, ResourceSaturation}; use ic_embedders::{ WasmtimeEmbedder, wasm_utils::compile, @@ -20,6 +20,7 @@ use ic_embedders::{ use ic_interfaces::execution_environment::{ ExecutionMode, HypervisorError, MessageMemoryUsage, SubnetAvailableMemory, SystemApi, }; +use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; use ic_logger::replica_logger::no_op_logger; use ic_management_canister_types_private::Global; use ic_registry_subnet_type::SubnetType; @@ -176,7 +177,10 @@ impl WasmtimeInstanceBuilder { Default::default(), self.api_type.caller(), self.api_type.call_context_id(), - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new( + SMALL_APP_SUBNET_MAX_SIZE, + CanisterCyclesCostSchedule::Normal, + ), ); let subnet_memory_capacity = i64::MAX / 2; diff --git a/rs/test_utilities/execution_environment/src/lib.rs b/rs/test_utilities/execution_environment/src/lib.rs index 33fdd2b0437a..4dc8435a14ab 100644 --- a/rs/test_utilities/execution_environment/src/lib.rs +++ b/rs/test_utilities/execution_environment/src/lib.rs @@ -8,7 +8,9 @@ use ic_config::{ subnet_config::{SubnetConfig, SubnetSecurity}, }; use ic_crypto_test_utils_reproducible_rng::ReproducibleRng; -use ic_cycles_account_manager::{CyclesAccountManager, ResourceSaturation}; +use ic_cycles_account_manager::{ + CyclesAccountManager, CyclesAccountManagerSubnetConfig, ResourceSaturation, +}; use ic_embedders::{ WasmtimeEmbedder, wasm_utils::{compile, decoding::decode_wasm}, @@ -433,6 +435,10 @@ impl ExecutionTest { self.state.as_ref().unwrap().get_own_cost_schedule() } + pub fn get_own_subnet_cycles_config(&self) -> CyclesAccountManagerSubnetConfig { + self.state.as_ref().unwrap().get_own_subnet_cycles_config() + } + pub fn executed_instructions(&self) -> NumInstructions { self.executed_instructions.values().sum() } @@ -455,7 +461,7 @@ impl ExecutionTest { .canister_snapshot_baseline_instructions .saturating_add(&new_snapshot_size.get().into()); self.cycles_account_manager - .management_canister_cost(instructions, self.subnet_size(), self.cost_schedule()) + .management_canister_cost(instructions, self.get_own_subnet_cycles_config()) .real() } @@ -487,8 +493,7 @@ impl ExecutionTest { memory_usage, message_memory_usage, compute_allocation, - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), ) } @@ -505,8 +510,7 @@ impl ExecutionTest { memory_usage, message_memory_usage, compute_allocation, - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), canister.system_state.reserved_balance(), ) } @@ -517,27 +521,24 @@ impl ExecutionTest { payload: &[u8], ) -> CompoundCycles { self.cycles_account_manager - .xnet_call_performed_fee(self.subnet_size(), self.cost_schedule()) + .xnet_call_performed_fee(self.get_own_subnet_cycles_config()) + self.cycles_account_manager.xnet_call_bytes_transmitted_fee( NumBytes::from((payload.len() + method_name.to_string().len()) as u64), - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), ) } pub fn max_response_fee(&self) -> CompoundCycles { self.cycles_account_manager.xnet_call_bytes_transmitted_fee( MAX_INTER_CANISTER_PAYLOAD_IN_BYTES, - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), ) } pub fn reply_fee(&self, payload: &[u8]) -> CompoundCycles { self.cycles_account_manager.xnet_call_bytes_transmitted_fee( NumBytes::from(payload.len() as u64), - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), ) } @@ -548,14 +549,13 @@ impl ExecutionTest { let bytes = reject_message.to_string().len() + std::mem::size_of::(); self.cycles_account_manager.xnet_call_bytes_transmitted_fee( NumBytes::from(bytes as u64), - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), ) } pub fn canister_creation_fee(&self) -> CompoundCycles { self.cycles_account_manager - .canister_creation_fee(self.subnet_size(), self.cost_schedule()) + .canister_creation_fee(self.get_own_subnet_cycles_config()) } pub fn http_request_fee( @@ -566,8 +566,7 @@ impl ExecutionTest { self.cycles_account_manager.http_request_fee( request_size, response_size_limit, - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), ) } @@ -593,8 +592,7 @@ impl ExecutionTest { self.cycles_account_manager .execution_cost( num_instructions, - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), WasmExecutionMode::Wasm32, // For this test, we can assume a Wasm32 execution. ) .real() @@ -1301,8 +1299,7 @@ impl ExecutionTest { self.time, &mut round_limits, self.resource_limits, - self.subnet_size(), - cost_schedule, + state.get_own_subnet_cycles_config(), ); self.subnet_available_memory = round_limits.subnet_available_memory; self.subnet_available_callbacks = round_limits.subnet_available_callbacks; @@ -1313,6 +1310,7 @@ impl ExecutionTest { canister_id, result.instructions_used.unwrap(), cost_schedule, + self.get_own_subnet_cycles_config(), ); self.check_invariants(); } @@ -1453,8 +1451,7 @@ impl ExecutionTest { network_topology, &mut round_limits, self.resource_limits, - self.subnet_size(), - cost_schedule, + state.get_own_subnet_cycles_config(), ); let (canister, response, instructions_used, heap_delta) = match result { ExecuteMessageResult::Finished { @@ -1472,7 +1469,12 @@ impl ExecutionTest { self.subnet_available_callbacks = round_limits.subnet_available_callbacks; state.metadata.heap_delta_estimate += heap_delta; - self.update_execution_stats(canister_id, instructions_used, cost_schedule); + self.update_execution_stats( + canister_id, + instructions_used, + cost_schedule, + state.get_own_subnet_cycles_config(), + ); state.put_canister_state(canister); self.state = Some(state); self.check_invariants(); @@ -1546,8 +1548,6 @@ impl ExecutionTest { 0, "expected_cycles_metrics_change assumes that some instructions were actually used" ); - let subnet_size = self.subnet_size(); - let cost_schedule = self.cost_schedule(); let message = match message { SubnetMessage::Response(_) => return NominalCycles::zero(), SubnetMessage::Request(request) => CanisterCall::Request(request), @@ -1599,8 +1599,7 @@ impl ExecutionTest { self.cycles_account_manager() .execution_cost( instructions_used, - subnet_size, - cost_schedule, + self.get_own_subnet_cycles_config(), execution_mode, ) .nominal() @@ -1614,8 +1613,7 @@ impl ExecutionTest { self.cycles_account_manager() .execution_cost( instructions_used, - subnet_size, - cost_schedule, + self.get_own_subnet_cycles_config(), execution_mode, ) .nominal() @@ -1627,7 +1625,7 @@ impl ExecutionTest { | Ok(Method::UploadCanisterSnapshotMetadata) | Ok(Method::UploadCanisterSnapshotData) => self .cycles_account_manager() - .management_canister_cost(instructions_used, subnet_size, cost_schedule) + .management_canister_cost(instructions_used, self.get_own_subnet_cycles_config()) .nominal(), _ => { // no instructions should be charged for other methods and thus @@ -1678,8 +1676,7 @@ impl ExecutionTest { } else { let baseline_cost = self.cycles_account_manager().execution_cost( NumInstructions::new(0), - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), WasmExecutionMode::Wasm32, ); // the base cost could still be charged in some cases even if no instructions @@ -1770,6 +1767,7 @@ impl ExecutionTest { canister_id, capped_slice_instructions_used, cost_schedule, + self.get_own_subnet_cycles_config(), ); } } @@ -1843,13 +1841,17 @@ impl ExecutionTest { self.time, &mut round_limits, self.resource_limits, - self.subnet_size(), - cost_schedule, + state.get_own_subnet_cycles_config(), ); state.metadata.heap_delta_estimate += result.heap_delta; self.subnet_available_memory = round_limits.subnet_available_memory; if let Some(instructions_used) = result.instructions_used { - self.update_execution_stats(canister_id, instructions_used, cost_schedule); + self.update_execution_stats( + canister_id, + instructions_used, + cost_schedule, + state.get_own_subnet_cycles_config(), + ); } canister = result.canister; if let Some(ir) = result.ingress_status { @@ -1916,7 +1918,6 @@ impl ExecutionTest { &canister_id, self.install_code_instruction_limits.clone(), &mut round_limits, - self.subnet_size(), ExecutionRound::from(0), ); let slice_instructions_used = @@ -1958,6 +1959,7 @@ impl ExecutionTest { canister_id, capped_instructions_used, cost_schedule, + self.get_own_subnet_cycles_config(), ); } ExecuteSubnetMessageResultType::Processing => { @@ -1989,14 +1991,18 @@ impl ExecutionTest { self.time, &mut round_limits, self.resource_limits, - self.subnet_size(), - cost_schedule, + state.get_own_subnet_cycles_config(), ); state.metadata.heap_delta_estimate += result.heap_delta; self.subnet_available_memory = round_limits.subnet_available_memory; self.subnet_available_callbacks = round_limits.subnet_available_callbacks; if let Some(instructions_used) = result.instructions_used { - self.update_execution_stats(canister_id, instructions_used, cost_schedule); + self.update_execution_stats( + canister_id, + instructions_used, + cost_schedule, + state.get_own_subnet_cycles_config(), + ); } canister = result.canister; if let Some(ir) = result.ingress_status { @@ -2032,6 +2038,7 @@ impl ExecutionTest { canister_id: CanisterId, executed: NumInstructions, cost_schedule: CanisterCyclesCostSchedule, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, ) { let mgr = &self.cycles_account_manager; *self @@ -2041,12 +2048,8 @@ impl ExecutionTest { let is_wasm64_execution = self.canister_wasm_execution_mode(canister_id); - let instruction_cost = mgr.execution_cost( - executed, - self.subnet_size(), - cost_schedule, - is_wasm64_execution, - ); + let instruction_cost = + mgr.execution_cost(executed, subnet_cycles_config, is_wasm64_execution); *self .execution_cost .entry(canister_id) @@ -2280,15 +2283,14 @@ impl ExecutionTest { .storage_reservation_cycles( allocated_bytes, subnet_memory_saturation, - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), ) .real() } pub fn prepayment_for_response_execution(&self, mode: WasmExecutionMode) -> Cycles { self.cycles_account_manager - .prepayment_for_response_execution(self.subnet_size(), self.cost_schedule(), mode) + .prepayment_for_response_execution(self.get_own_subnet_cycles_config(), mode) .real() } @@ -2296,15 +2298,14 @@ impl ExecutionTest { let no_op_counter: IntCounter = IntCounter::new("no_op", "no_op").unwrap(); let prepayment_for_response_transmission = self .cycles_account_manager - .prepayment_for_response_transmission(self.subnet_size(), self.cost_schedule()); + .prepayment_for_response_transmission(self.get_own_subnet_cycles_config()); self.cycles_account_manager .refund_for_response_transmission( &self.log, &no_op_counter, response, prepayment_for_response_transmission, - self.subnet_size(), - self.cost_schedule(), + self.get_own_subnet_cycles_config(), ) .real() } diff --git a/rs/tests/execution/general_execution_tests/canister_lifecycle.rs b/rs/tests/execution/general_execution_tests/canister_lifecycle.rs index f50c6de3f81e..ae8b486a7216 100644 --- a/rs/tests/execution/general_execution_tests/canister_lifecycle.rs +++ b/rs/tests/execution/general_execution_tests/canister_lifecycle.rs @@ -107,7 +107,7 @@ pub fn update_settings_of_frozen_canister(env: TestEnv) { use ic_base_types::NumBytes; use ic_cdk::api::management_canister::main::{CanisterSettings, UpdateSettingsArgument}; use ic_config::subnet_config::{CyclesAccountManagerConfig, SchedulerConfig, SubnetSecurity}; - use ic_cycles_account_manager::CyclesAccountManager; + use ic_cycles_account_manager::{CyclesAccountManager, CyclesAccountManagerSubnetConfig}; let logger = env.logger(); let app_node = env.get_first_healthy_application_node_snapshot(); @@ -229,8 +229,10 @@ pub fn update_settings_of_frozen_canister(env: TestEnv) { > cycles_account_manager .ingress_induction_cost_from_bytes( NumBytes::new(bytes.len() as u64), - 1, - CanisterCyclesCostSchedule::Normal + CyclesAccountManagerSubnetConfig::new( + 1, + CanisterCyclesCostSchedule::Normal, + ), ) .real() .get() diff --git a/rs/tests/networking/BUILD.bazel b/rs/tests/networking/BUILD.bazel index e442a0966d6a..8afa88ca85ab 100644 --- a/rs/tests/networking/BUILD.bazel +++ b/rs/tests/networking/BUILD.bazel @@ -141,6 +141,7 @@ system_test_nns( "PROXY_WASM_PATH": "//rs/rust_canisters/proxy_canister:proxy_canister", }, deps = CANISTER_HTTP_BASE_DEPS + [ + "//rs/cycles_account_manager", "//rs/registry/subnet_type", "//rs/rust_canisters/canister_test", "//rs/test_utilities", diff --git a/rs/tests/networking/Cargo.toml b/rs/tests/networking/Cargo.toml index 52e350b03374..683a0931998d 100644 --- a/rs/tests/networking/Cargo.toml +++ b/rs/tests/networking/Cargo.toml @@ -26,6 +26,7 @@ ic-http-endpoints-public = { path = "../../http_endpoints/public" } ic-http-endpoints-test-agent = { path = "../../http_endpoints/test_agent" } ic_consensus_system_test_utils = { path = "../consensus/utils" } ic_consensus_threshold_sig_system_test_utils = { path = "../consensus/tecdsa/utils" } +ic-cycles-account-manager = { path = "../../cycles_account_manager" } ic-limits = { path = "../../limits" } ic-management-canister-types-private = { path = "../../types/management_canister_types" } ic-message = { path = "../test_canisters/message" } diff --git a/rs/tests/networking/canister_http_correctness_test.rs b/rs/tests/networking/canister_http_correctness_test.rs index 7eab79498d41..fc2af516d67c 100644 --- a/rs/tests/networking/canister_http_correctness_test.rs +++ b/rs/tests/networking/canister_http_correctness_test.rs @@ -27,6 +27,7 @@ use ic_agent::{ }; use ic_base_types::{CanisterId, NumBytes, PrincipalId}; use ic_cdk::api::call::RejectionCode; +use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_management_canister_types_private::{ HttpHeader, HttpMethod, TransformContext, TransformFunc, }; @@ -2724,8 +2725,7 @@ fn expected_cycle_cost( let cycle_fee = cm.http_request_fee( req_size, Some(NumBytes::from(response_size)), - subnet_size, - CanisterCyclesCostSchedule::Normal, + CyclesAccountManagerSubnetConfig::new(subnet_size, CanisterCyclesCostSchedule::Normal), ); cycle_fee.real().get().try_into().unwrap() } diff --git a/rs/types/cycles/src/cycles_account_manager_subnet_config.rs b/rs/types/cycles/src/cycles_account_manager_subnet_config.rs new file mode 100644 index 000000000000..42be3be395d9 --- /dev/null +++ b/rs/types/cycles/src/cycles_account_manager_subnet_config.rs @@ -0,0 +1,18 @@ +use crate::CanisterCyclesCostSchedule; +use serde::{Deserialize, Serialize}; + +/// Groups the subnet configuration parameters needed for cycle cost calculations. +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct CyclesAccountManagerSubnetConfig { + pub subnet_size: usize, + pub cost_schedule: CanisterCyclesCostSchedule, +} + +impl CyclesAccountManagerSubnetConfig { + pub fn new(subnet_size: usize, cost_schedule: CanisterCyclesCostSchedule) -> Self { + Self { + subnet_size, + cost_schedule, + } + } +} diff --git a/rs/types/cycles/src/lib.rs b/rs/types/cycles/src/lib.rs index 8607cc920a8d..956ebd354048 100644 --- a/rs/types/cycles/src/lib.rs +++ b/rs/types/cycles/src/lib.rs @@ -1,11 +1,13 @@ mod compound_cycles; mod cycles; +mod cycles_account_manager_subnet_config; mod cycles_cost_schedule; mod cycles_use_case; mod nominal_cycles; pub use compound_cycles::CompoundCycles; pub use cycles::Cycles; +pub use cycles_account_manager_subnet_config::CyclesAccountManagerSubnetConfig; pub use cycles_cost_schedule::CanisterCyclesCostSchedule; pub use cycles_use_case::{ BurnedCycles, CanisterCreation, ComputeAllocation, CyclesUseCase, CyclesUseCaseKind, From 3a578484579b2fbba444a5290b020029802e482b Mon Sep 17 00:00:00 2001 From: Pierugo Pace Date: Wed, 17 Jun 2026 15:53:53 +0200 Subject: [PATCH 58/75] docs: add missing firewall rule scope in `ic-admin` docs (#10498) This tiny PR adds `api_boundary_nodes` as possible scope when fetching firewall rules in `ic-admin`'s documentation (see [source](https://sourcegraph.com/r/github.com/dfinity/ic@58168c8e1c6373a5e439729af87ddebebe369622/-/blob/rs/registry/keys/src/lib.rs?L192)). --- rs/registry/admin/bin/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/registry/admin/bin/main.rs b/rs/registry/admin/bin/main.rs index ca93e30ef416..6f3e393d85b8 100644 --- a/rs/registry/admin/bin/main.rs +++ b/rs/registry/admin/bin/main.rs @@ -3351,7 +3351,7 @@ impl ProposalPayload for ProposeToUpdateFirewallRule /// Sub-command to get all firewall rules for a given scope. #[derive(Parser)] struct GetFirewallRulesCmd { - /// The scope to apply new rules at (can be "global", "replica_nodes", "subnet(id)", or "node(id)") + /// The scope to apply new rules at (can be "global", "replica_nodes", "api_boundary_nodes", "subnet(id)", or "node(id)") pub scope: FirewallRulesScope, } From 7e9db218e007d22c0fc097923f201c2f46a95caa Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Wed, 17 Jun 2026 08:27:43 -0700 Subject: [PATCH 59/75] fix: Fix python bazel wrapper after upgrade (#10479) After https://github.com/dfinity/ic/pull/10413 upgraded `rules_python`, nightly bare metal jobs started failing. This updates how we bazel wrap our tools, to match the new environment format. --- ic-os/dev-tools/bare_metal_deployment/deploy.py | 2 +- ic-os/dev-tools/bare_metal_deployment/tools.bzl | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ic-os/dev-tools/bare_metal_deployment/deploy.py b/ic-os/dev-tools/bare_metal_deployment/deploy.py index fe08463fc5ab..5b19c413f3b0 100755 --- a/ic-os/dev-tools/bare_metal_deployment/deploy.py +++ b/ic-os/dev-tools/bare_metal_deployment/deploy.py @@ -431,7 +431,7 @@ def gen_failure(result: invoke.Result, bmc_info: BMCInfo) -> DeploymentError: def run_script(idrac_script_dir: Path, bmc_info: BMCInfo, script_and_args: str, permissive: bool = True) -> None: """Run a given script from the given bin dir and raise an exception if anything went wrong""" - command = f"python3 {idrac_script_dir}/{script_and_args}" + command = f"{sys.executable} {idrac_script_dir}/{script_and_args}" result = invoke.run(command) if result and not result.ok: diff --git a/ic-os/dev-tools/bare_metal_deployment/tools.bzl b/ic-os/dev-tools/bare_metal_deployment/tools.bzl index 53660c122510..962fc47d4a0f 100644 --- a/ic-os/dev-tools/bare_metal_deployment/tools.bzl +++ b/ic-os/dev-tools/bare_metal_deployment/tools.bzl @@ -22,13 +22,15 @@ def launch_bare_metal(name, image_zst_file): requirement("simple-parsing"), requirement("tqdm"), ], + config_settings = { + "@rules_python//python/config_settings:bootstrap_impl": "script", + }, tags = ["manual"], ) sh_binary( name = name, srcs = ["//toolchains/sysimage:proc_wrapper.sh"], args = [ - "python3", "$(location :" + binary_name + ")", "--inject_configuration_tool", "$(location //rs/ic_os/dev_test_tools/setupos-image-config:setupos-inject-config)", From 397aada848359e784b91658ce8f5bcbd4537c6f3 Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Thu, 18 Jun 2026 11:22:32 +0200 Subject: [PATCH 60/75] feat: hoist e2fsdroid (from platform-tools) from Dockerfile to Bazel (#10490) Hoist e2fsdroid out of the Dockerfile and into the Bazel build, like we did for e2fsprogs (mke2fs), dosfstools and mtools. This will still pull the prebuilt binary; we don't build it from source (yet). Instead, this pins the platform-tools archive directly via http_archive and exposes just the prebuilt e2fsdroid binary as //:e2fsdroid. Wire it into the ext4_image rule and build_ext4_image.py the same way as //:mkfs.ext4, so the image build no longer relies on e2fsdroid being on PATH. --------- Co-authored-by: IDX GitHub Automation <> --- .devcontainer/devcontainer.json | 2 +- .github/workflows/api-bn-recovery-test.yml | 2 +- .github/workflows/ci-main.yml | 2 +- .github/workflows/ci-pr-only.yml | 2 +- .../workflows/container-api-bn-recovery.yml | 2 +- .github/workflows/container-scan-nightly.yml | 2 +- .github/workflows/pocket-ic-tests-windows.yml | 2 +- .../workflows/rate-limits-backend-release.yml | 2 +- .github/workflows/release-testing.yml | 2 +- .github/workflows/rosetta-release.yml | 2 +- .../salt-sharing-canister-release.yml | 2 +- .github/workflows/schedule-daily.yml | 2 +- .github/workflows/schedule-rust-bench.yml | 2 +- .../system-tests-benchmarks-nightly.yml | 2 +- .../update-mainnet-canister-revisions.yaml | 2 +- BUILD.bazel | 10 ++++++++++ MODULE.bazel | 10 ++++++++++ ci/container/Dockerfile | 20 ------------------- ci/container/TAG | 2 +- third_party/BUILD.e2fsdroid.bazel | 11 ++++++++++ toolchains/sysimage/build_ext4_image.py | 5 ++++- toolchains/sysimage/toolchain.bzl | 10 ++++++++++ 22 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 third_party/BUILD.e2fsdroid.bazel diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bac981218653..79a0c8945448 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "image": "ghcr.io/dfinity/ic-dev@sha256:668279aab052f00846e98c8cf9d37493d6b3e5ea10ffa4d0840b468219e1f44d", + "image": "ghcr.io/dfinity/ic-dev@sha256:5a736731cd2b1495c90acc33f89ca13ddb48b1d5da59b31fc6235e2f6891ff2c", "remoteUser": "ubuntu", "privileged": true, "runArgs": [ diff --git a/.github/workflows/api-bn-recovery-test.yml b/.github/workflows/api-bn-recovery-test.yml index 1c6111fed8be..698633d08676 100644 --- a/.github/workflows/api-bn-recovery-test.yml +++ b/.github/workflows/api-bn-recovery-test.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 0d9ecc46e0e3..ad5ee427e909 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -33,7 +33,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/ci-pr-only.yml b/.github/workflows/ci-pr-only.yml index 6978173149fb..98e5712781f2 100644 --- a/.github/workflows/ci-pr-only.yml +++ b/.github/workflows/ci-pr-only.yml @@ -37,7 +37,7 @@ jobs: runs-on: &dind-small-setup labels: dind-small container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --mount type=tmpfs,target="/tmp/containers" steps: diff --git a/.github/workflows/container-api-bn-recovery.yml b/.github/workflows/container-api-bn-recovery.yml index ae8e02d25f3a..a60b050d552f 100644 --- a/.github/workflows/container-api-bn-recovery.yml +++ b/.github/workflows/container-api-bn-recovery.yml @@ -28,7 +28,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/home/buildifier/.local/share/containers" diff --git a/.github/workflows/container-scan-nightly.yml b/.github/workflows/container-scan-nightly.yml index 18122156ffac..a057a4a4fc39 100644 --- a/.github/workflows/container-scan-nightly.yml +++ b/.github/workflows/container-scan-nightly.yml @@ -12,7 +12,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 60 diff --git a/.github/workflows/pocket-ic-tests-windows.yml b/.github/workflows/pocket-ic-tests-windows.yml index fdeba78188a2..40c3a0c89c6a 100644 --- a/.github/workflows/pocket-ic-tests-windows.yml +++ b/.github/workflows/pocket-ic-tests-windows.yml @@ -45,7 +45,7 @@ jobs: bazel-build-pocket-ic: name: Bazel Build PocketIC container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 90 diff --git a/.github/workflows/rate-limits-backend-release.yml b/.github/workflows/rate-limits-backend-release.yml index 1cfdae770a5d..428289ae7ba1 100644 --- a/.github/workflows/rate-limits-backend-release.yml +++ b/.github/workflows/rate-limits-backend-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/release-testing.yml b/.github/workflows/release-testing.yml index aa48f1b2392e..2b075f0d452e 100644 --- a/.github/workflows/release-testing.yml +++ b/.github/workflows/release-testing.yml @@ -35,7 +35,7 @@ jobs: group: dm1 labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 180 diff --git a/.github/workflows/rosetta-release.yml b/.github/workflows/rosetta-release.yml index 50a6d30cf0c2..612f0551df01 100644 --- a/.github/workflows/rosetta-release.yml +++ b/.github/workflows/rosetta-release.yml @@ -22,7 +22,7 @@ jobs: runs-on: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" environment: DockerHub diff --git a/.github/workflows/salt-sharing-canister-release.yml b/.github/workflows/salt-sharing-canister-release.yml index a88e5bb8de5b..b47f0660819d 100644 --- a/.github/workflows/salt-sharing-canister-release.yml +++ b/.github/workflows/salt-sharing-canister-release.yml @@ -32,7 +32,7 @@ jobs: labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" diff --git a/.github/workflows/schedule-daily.yml b/.github/workflows/schedule-daily.yml index 661098f10c54..7ab8bf80f622 100644 --- a/.github/workflows/schedule-daily.yml +++ b/.github/workflows/schedule-daily.yml @@ -14,7 +14,7 @@ jobs: runs-on: &dind-large-setup labels: dind-large container: &container-setup - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/schedule-rust-bench.yml b/.github/workflows/schedule-rust-bench.yml index 32deb5280b77..ce37d441e637 100644 --- a/.github/workflows/schedule-rust-bench.yml +++ b/.github/workflows/schedule-rust-bench.yml @@ -24,7 +24,7 @@ jobs: # see linux-x86-64 runner group labels: rust-benchmarks container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d # running on bare metal machine using ubuntu user options: --user ubuntu --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 720 # 12 hours diff --git a/.github/workflows/system-tests-benchmarks-nightly.yml b/.github/workflows/system-tests-benchmarks-nightly.yml index 9c79e0cb89ca..7873d9b327a3 100644 --- a/.github/workflows/system-tests-benchmarks-nightly.yml +++ b/.github/workflows/system-tests-benchmarks-nightly.yml @@ -17,7 +17,7 @@ jobs: group: dm1 labels: dind-large container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host --mount type=tmpfs,target="/tmp/containers" timeout-minutes: 480 diff --git a/.github/workflows/update-mainnet-canister-revisions.yaml b/.github/workflows/update-mainnet-canister-revisions.yaml index 11cfcd394e49..d49003b2b05f 100644 --- a/.github/workflows/update-mainnet-canister-revisions.yaml +++ b/.github/workflows/update-mainnet-canister-revisions.yaml @@ -25,7 +25,7 @@ jobs: labels: dind-small environment: CREATE_PR container: - image: ghcr.io/dfinity/ic-build@sha256:11d1df3bfc3e4edf1c28c93f4433dc28b004b3391eab05e735e597914957f0be + image: ghcr.io/dfinity/ic-build@sha256:b16b998b189477b54507597f57c5acc146483b0e1ce72b62beae5edd15ff094d options: >- -e NODE_NAME --privileged --cgroupns host -v /var/tmp:/var/tmp -v /ceph-s3-info:/ceph-s3-info --mount type=tmpfs,target="/tmp/containers" env: diff --git a/BUILD.bazel b/BUILD.bazel index ef16436f8d27..68e98bac510b 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -248,6 +248,16 @@ alias( }), ) +### e2fsdroid, used to populate ext4 filesystem images (fs_config + SELinux contexts) + +alias( + name = "e2fsdroid", + actual = select({ + "@bazel_tools//src/conditions:linux_x86_64": "@android_platform_tools//:e2fsdroid", + "//conditions:default": "@platforms//:incompatible", + }), +) + ### shfmt, used to format bash code alias( diff --git a/MODULE.bazel b/MODULE.bazel index e78debdc2234..ec0a3b8ca89a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -767,6 +767,16 @@ http_archive( ], ) +# e2fsdroid, used to populate ext4 filesystem images for IC-OS. +# We use the prebuilt binary from android platform-tools. +http_archive( + name = "android_platform_tools", + build_file = "@//third_party:BUILD.e2fsdroid.bazel", + sha256 = "defcee9da1f22fe5c2324ec0edf612122f1c6ffe01a7b124191e07fcc74f8fff", + strip_prefix = "platform-tools", + urls = ["https://dl.google.com/android/repository/platform-tools_r33.0.2-linux.zip"], +) + http_archive( name = "lmdb", build_file = "@//third_party:BUILD.lmdb.bazel", diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile index d53de14b2404..4900a2c606da 100644 --- a/ci/container/Dockerfile +++ b/ci/container/Dockerfile @@ -30,26 +30,6 @@ RUN groupadd -g 1001 buildifier && useradd -ms /bin/bash -u 1001 -g 1001 -G ubun # both buildifier and ubuntu users need to be able to access root permissions echo "ALL ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers -# Install a version of google-android-platform-tools-installer with e2fsdroid -RUN export DEBIAN_FRONTEND=noninteractive && \ - mkdir e2fsdroid && \ - cd e2fsdroid && \ - curl -fsSLO http://mirrors.kernel.org/ubuntu/pool/multiverse/g/google-android-installers/google-android-platform-tools-installer_35.0.0+1710437545-3build2_amd64.deb && \ - curl -fsSLO http://mirrors.kernel.org/ubuntu/pool/multiverse/g/google-android-installers/google-android-licenses_1710437545-3build2_all.deb && \ - dpkg-deb -R google-android-platform-tools-installer_35.0.0+1710437545-3build2_amd64.deb contents && \ - cd contents && \ - sed -i 's/35.0.0/33.0.2/' DEBIAN/control DEBIAN/postinst && \ - sed -i '/Version/ s/$/+downgraded/' DEBIAN/control && \ - rm usr/share/google-android-platform-tools-installer/platform-tools_r35.0.0-linux.zip.sha1 && \ - echo "6bf4f747ad929b02378b44ce083b4502d26109c7 platform-tools_r33.0.2-linux.zip" > usr/share/google-android-platform-tools-installer/platform-tools_r33.0.2-linux.zip.sha1 && \ - find . -type f -not -path "./DEBIAN/*" -exec md5sum {} + | sort -k 2 | sed 's/\.\/\(.*\)/\1/' > DEBIAN/md5sums && \ - cd .. && \ - dpkg-deb -b contents/ downgraded.deb && \ - apt -yqq install ./downgraded.deb ./google-android-licenses_1710437545-3build2_all.deb && \ - apt-mark hold google-android-platform-tools-installer && \ - cd .. && \ - rm -rf e2fsdroid - ARG bazelisk_sha=22e7d3a188699982f661cf4687137ee52d1f24fec1ec893d91a6c4d791a75de8 RUN curl -fsSL https://github.com/bazelbuild/bazelisk/releases/download/v1.28.1/bazelisk-linux-amd64 -o /usr/bin/bazel && \ echo "$bazelisk_sha /usr/bin/bazel" | sha256sum --check && \ diff --git a/ci/container/TAG b/ci/container/TAG index 7e62c208d040..e6e1ec7687a4 100644 --- a/ci/container/TAG +++ b/ci/container/TAG @@ -1 +1 @@ -3cfb71e6fe1f5830eb736eda108e465dc52ee2917f36c3ac4a14bbc09fd07063 +262ce3195d513b530614e2b6f1d77e0fb974cf207eb300d4ea4fa8766db09ce1 diff --git a/third_party/BUILD.e2fsdroid.bazel b/third_party/BUILD.e2fsdroid.bazel new file mode 100644 index 000000000000..944736704afe --- /dev/null +++ b/third_party/BUILD.e2fsdroid.bazel @@ -0,0 +1,11 @@ +""" +e2fsdroid, taken from the prebuilt Android SDK platform-tools (r33.0.2). + +IC-OS uses it (in build_ext4_image.py) to populate an ext4 image from a directory +tree while applying an Android-style fs_config (per-file uid/gid/mode, via `-C`), +SELinux file contexts (`-S`), and deterministic timestamps (`-T 0`). +""" + +package(default_visibility = ["//visibility:public"]) + +exports_files(["e2fsdroid"]) diff --git a/toolchains/sysimage/build_ext4_image.py b/toolchains/sysimage/build_ext4_image.py index 5c0b5d8acb09..ca50297f8105 100755 --- a/toolchains/sysimage/build_ext4_image.py +++ b/toolchains/sysimage/build_ext4_image.py @@ -168,6 +168,7 @@ def make_argparser(): parser.add_argument("--diroid", help="Path to our diroid tool", type=str, required=True) parser.add_argument("--zstd", help="Path to the zstd tool", type=str, required=True) parser.add_argument("--mkfs-ext4", help="Path to the mkfs.ext4 (mke2fs) tool", type=str, required=True) + parser.add_argument("--e2fsdroid", help="Path to the e2fsdroid tool", type=str, required=True) return parser @@ -249,7 +250,9 @@ def main(): "fakeroot", "-i", fakeroot_statefile, - "e2fsdroid", + # Absolute path so fakeroot (which execs it) resolves it as a path rather + # than searching PATH. + os.path.abspath(args.e2fsdroid), "-e", "-a", "/", diff --git a/toolchains/sysimage/toolchain.bzl b/toolchains/sysimage/toolchain.bzl index 45c7071461b9..9f4367a92be2 100644 --- a/toolchains/sysimage/toolchain.bzl +++ b/toolchains/sysimage/toolchain.bzl @@ -383,6 +383,9 @@ def _ext4_image_impl(ctx): fail("could not locate mke2fs binary among //:mkfs.ext4 outputs") inputs.extend(ctx.files._mkfs_ext4) + e2fsdroid = _find_tool(ctx.files._e2fsdroid, "e2fsdroid") + inputs.extend(ctx.files._e2fsdroid) + args.extend([ "-s", ctx.attr.partition_size, @@ -396,6 +399,8 @@ def _ext4_image_impl(ctx): ctx.executable._zstd.path, "--mkfs-ext4", mke2fs.path, + "--e2fsdroid", + e2fsdroid.path, ]) if ctx.attr.file_contexts: @@ -451,6 +456,11 @@ ext4_image = _icos_build_rule( cfg = "exec", allow_files = True, ), + "_e2fsdroid": attr.label( + default = "//:e2fsdroid", + cfg = "exec", + allow_files = True, + ), "_diroid": attr.label( default = "//rs/ic_os/build_tools/diroid", executable = True, From ce26e69f2dd262a893e511889590ff39942650d9 Mon Sep 17 00:00:00 2001 From: Daniel Wong <97631336+daniel-wong-dfinity-org@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:24:19 +0200 Subject: [PATCH 61/75] chore(governance): Get rid of ENABLE_CREATE_CANISTER_AND_INSTALL_CODE_PROPOSALS flag. (#9886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # References Closes [NNS1-4347]. [NNS1-4347]: https://dfinity.atlassian.net/browse/NNS1-4347 [👈 Previous PR][prev] [prev]: https://github.com/dfinity/ic/pull/9826 --- .../tests/create_canister_and_install_code.rs | 4 +--- rs/nns/governance/src/lib.rs | 17 -------------- .../create_canister_and_install_code.rs | 22 ------------------- 3 files changed, 1 insertion(+), 42 deletions(-) diff --git a/rs/nervous_system/integration_tests/tests/create_canister_and_install_code.rs b/rs/nervous_system/integration_tests/tests/create_canister_and_install_code.rs index c39c38bf4f58..8888e83eeaa6 100644 --- a/rs/nervous_system/integration_tests/tests/create_canister_and_install_code.rs +++ b/rs/nervous_system/integration_tests/tests/create_canister_and_install_code.rs @@ -28,12 +28,10 @@ async fn test_create_canister_and_install_code() { .build_async() .await; - // Step 1.2: Install NNS canisters with the test governance canister - // (which has the CreateCanisterAndInstallCode feature flag enabled). + // Step 1.2: Install NNS canisters. { let mut nns_installer = NnsInstaller::default(); nns_installer.with_current_nns_canister_versions(); - nns_installer.with_test_governance_canister(); nns_installer.install(&pocket_ic).await; } diff --git a/rs/nns/governance/src/lib.rs b/rs/nns/governance/src/lib.rs index 2aef49af163d..538ea0db3a1d 100644 --- a/rs/nns/governance/src/lib.rs +++ b/rs/nns/governance/src/lib.rs @@ -210,9 +210,6 @@ thread_local! { static ENABLE_NEURON_FOLLOW_RESTRICTIONS: Cell = const { Cell::new(true) }; - static ENABLE_CREATE_CANISTER_AND_INSTALL_CODE_PROPOSALS: Cell - = const { Cell::new(true) }; - static ENABLE_SUBNET_SPLITTING_PROPOSALS: Cell = const { Cell::new(false) }; @@ -285,20 +282,6 @@ pub fn temporarily_disable_neuron_follow_restrictions() -> Temporary { Temporary::new(&ENABLE_NEURON_FOLLOW_RESTRICTIONS, false) } -pub fn are_create_canister_and_install_code_proposals_enabled() -> bool { - ENABLE_CREATE_CANISTER_AND_INSTALL_CODE_PROPOSALS.get() -} - -#[cfg(any(test, feature = "canbench-rs", feature = "test"))] -pub fn temporarily_enable_create_canister_and_install_code_proposals() -> Temporary { - Temporary::new(&ENABLE_CREATE_CANISTER_AND_INSTALL_CODE_PROPOSALS, true) -} - -#[cfg(any(test, feature = "canbench-rs", feature = "test"))] -pub fn temporarily_disable_create_canister_and_install_code_proposals() -> Temporary { - Temporary::new(&ENABLE_CREATE_CANISTER_AND_INSTALL_CODE_PROPOSALS, false) -} - #[cfg(any(test, feature = "canbench-rs", feature = "test"))] pub fn temporarily_enable_subnet_splitting_proposals() -> Temporary { Temporary::new(&ENABLE_SUBNET_SPLITTING_PROPOSALS, true) diff --git a/rs/nns/governance/src/proposals/create_canister_and_install_code.rs b/rs/nns/governance/src/proposals/create_canister_and_install_code.rs index 4945e479bf19..63bacb50b564 100644 --- a/rs/nns/governance/src/proposals/create_canister_and_install_code.rs +++ b/rs/nns/governance/src/proposals/create_canister_and_install_code.rs @@ -1,5 +1,4 @@ use crate::{ - are_create_canister_and_install_code_proposals_enabled, pb::v1::{ CreateCanisterAndInstallCode, GovernanceError, SelfDescribingValue, Topic, canister_settings::{LogVisibility, SnapshotVisibility}, @@ -23,13 +22,6 @@ use ic_nns_handler_root_interface as root; impl CreateCanisterAndInstallCode { pub fn validate(&self) -> Result<(), GovernanceError> { - if !are_create_canister_and_install_code_proposals_enabled() { - return Err(GovernanceError::new_with_message( - ErrorType::InvalidProposal, - "CreateCanisterAndInstallCode proposals are not enabled yet.", - )); - } - let Self { host_subnet_id, canister_settings, @@ -226,24 +218,10 @@ impl CallCanister for CreateCanisterAndInstallCode { type Reply = root::CreateCanisterAndInstallCodeOk; fn canister_and_function(&self) -> Result<(CanisterId, &str), GovernanceError> { - if !are_create_canister_and_install_code_proposals_enabled() { - return Err(GovernanceError::new_with_message( - ErrorType::InvalidProposal, - "CreateCanisterAndInstallCode proposals are not enabled yet.", - )); - } - Ok((ROOT_CANISTER_ID, "create_canister_and_install_code")) } fn payload(&self) -> Result, GovernanceError> { - if !are_create_canister_and_install_code_proposals_enabled() { - return Err(GovernanceError::new_with_message( - ErrorType::InvalidProposal, - "CreateCanisterAndInstallCode proposals are not enabled yet.", - )); - } - let request = root::CreateCanisterAndInstallCodeRequest::try_from(self.clone())?; Encode!(&request).map_err(|e| { From 34e5e8957afd28676dc0264a3f3e8ba19d764b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Thu, 18 Jun 2026 13:37:58 +0200 Subject: [PATCH 62/75] test(ICRC_Ledger): DEFI-2646: Add canbench for get_blocks in ICRC ledger (#8653) Add a `canbench` benchmark for the `get_blocks` endpoint in the ICRC ledger. --- .../ledger/canbench_results/canbench_u256.yml | 19 +++++++----- .../ledger/canbench_results/canbench_u64.yml | 31 +++++++++++-------- .../icrc1/ledger/src/benches/benches_u256.rs | 12 +++++++ .../icrc1/ledger/src/benches/benches_u64.rs | 12 +++++++ 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml b/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml index e5317cbf1a2f..fc2605b336aa 100644 --- a/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml +++ b/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml @@ -2,10 +2,15 @@ benches: bench_icrc1_transfers: total: calls: 1 - instructions: 54515026271 + instructions: 54522585409 heap_increase: 264 stable_memory_increase: 256 scopes: + get_blocks: + calls: 1 + instructions: 7558234 + heap_increase: 0 + stable_memory_increase: 0 icrc103_get_allowances: calls: 1 instructions: 6379538 @@ -28,12 +33,12 @@ benches: stable_memory_increase: 0 icrc3_get_blocks: calls: 1 - instructions: 8712614 + instructions: 8712670 heap_increase: 0 stable_memory_increase: 0 post_upgrade: calls: 1 - instructions: 351358365 + instructions: 351357590 heap_increase: 71 stable_memory_increase: 0 pre_upgrade: @@ -43,19 +48,19 @@ benches: stable_memory_increase: 128 upgrade: calls: 1 - instructions: 501339592 + instructions: 501338973 heap_increase: 200 stable_memory_increase: 128 bench_upgrade_baseline: total: calls: 1 - instructions: 8695232 + instructions: 8695231 heap_increase: 258 stable_memory_increase: 128 scopes: post_upgrade: calls: 1 - instructions: 8613828 + instructions: 8613827 heap_increase: 129 stable_memory_increase: 0 pre_upgrade: @@ -65,7 +70,7 @@ benches: stable_memory_increase: 128 upgrade: calls: 1 - instructions: 8694324 + instructions: 8694323 heap_increase: 258 stable_memory_increase: 128 version: 0.4.1 diff --git a/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u64.yml b/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u64.yml index b4bdf562373f..48c161b4dee8 100644 --- a/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u64.yml +++ b/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u64.yml @@ -2,70 +2,75 @@ benches: bench_icrc1_transfers: total: calls: 1 - instructions: 52115351782 + instructions: 52121937493 heap_increase: 263 stable_memory_increase: 256 scopes: + get_blocks: + calls: 1 + instructions: 7117169 + heap_increase: 0 + stable_memory_increase: 0 icrc103_get_allowances: calls: 1 - instructions: 5652628 + instructions: 5652590 heap_increase: 0 stable_memory_increase: 0 icrc1_transfer: calls: 1 - instructions: 12277125048 + instructions: 12276507490 heap_increase: 34 stable_memory_increase: 0 icrc2_approve: calls: 1 - instructions: 18496271005 + instructions: 18496842528 heap_increase: 25 stable_memory_increase: 128 icrc2_transfer_from: calls: 1 - instructions: 20648355831 + instructions: 20647878061 heap_increase: 3 stable_memory_increase: 0 icrc3_get_blocks: calls: 1 - instructions: 8197351 + instructions: 8197407 heap_increase: 0 stable_memory_increase: 0 post_upgrade: calls: 1 - instructions: 342206912 + instructions: 342206791 heap_increase: 72 stable_memory_increase: 0 pre_upgrade: calls: 1 - instructions: 149978847 + instructions: 149978905 heap_increase: 129 stable_memory_increase: 128 upgrade: calls: 1 - instructions: 492188500 + instructions: 492188724 heap_increase: 201 stable_memory_increase: 128 bench_upgrade_baseline: total: calls: 1 - instructions: 8691827 + instructions: 8691889 heap_increase: 258 stable_memory_increase: 128 scopes: post_upgrade: calls: 1 - instructions: 8609300 + instructions: 8609302 heap_increase: 129 stable_memory_increase: 0 pre_upgrade: calls: 1 - instructions: 79603 + instructions: 79663 heap_increase: 129 stable_memory_increase: 128 upgrade: calls: 1 - instructions: 8690919 + instructions: 8690981 heap_increase: 258 stable_memory_increase: 128 version: 0.4.1 diff --git a/rs/ledger_suite/icrc1/ledger/src/benches/benches_u256.rs b/rs/ledger_suite/icrc1/ledger/src/benches/benches_u256.rs index d38b2f75a512..a87ac16e6106 100644 --- a/rs/ledger_suite/icrc1/ledger/src/benches/benches_u256.rs +++ b/rs/ledger_suite/icrc1/ledger/src/benches/benches_u256.rs @@ -1,3 +1,5 @@ +#[cfg(not(feature = "get-blocks-disabled"))] +use crate::get_blocks; use crate::{ Access, Account, LOG, benches::{ @@ -116,6 +118,16 @@ fn bench_icrc1_transfers() -> BenchResult { let blocks_res = icrc3_get_blocks(vec![req]); assert_eq!(blocks_res.blocks.len(), NUM_GET_BLOCKS as usize); } + #[cfg(not(feature = "get-blocks-disabled"))] + { + let req = GetBlocksRequest { + start: Nat::from(3 * NUM_OPERATIONS), + length: Nat::from(NUM_GET_BLOCKS), + }; + let _p = canbench_rs::bench_scope("get_blocks"); + let blocks_res = get_blocks(req); + assert_eq!(blocks_res.blocks.len(), NUM_GET_BLOCKS as usize); + } upgrade(); }) } diff --git a/rs/ledger_suite/icrc1/ledger/src/benches/benches_u64.rs b/rs/ledger_suite/icrc1/ledger/src/benches/benches_u64.rs index 72b9ad5ef6b3..771d7bdd4fa9 100644 --- a/rs/ledger_suite/icrc1/ledger/src/benches/benches_u64.rs +++ b/rs/ledger_suite/icrc1/ledger/src/benches/benches_u64.rs @@ -2,6 +2,8 @@ use crate::benches::{ MAX_LIST_ALLOWANCES, NUM_GET_BLOCKS, NUM_OPERATIONS, assert_has_num_balances, emulate_archive_blocks, icrc_transfer, mint_tokens, test_account, test_account_offset, upgrade, }; +#[cfg(not(feature = "get-blocks-disabled"))] +use crate::get_blocks; use crate::{ Access, LOG, icrc2_approve_not_async, icrc3_get_blocks, icrc103_get_allowances, init_state, }; @@ -115,6 +117,16 @@ fn bench_icrc1_transfers() -> BenchResult { let blocks_res = icrc3_get_blocks(vec![req]); assert_eq!(blocks_res.blocks.len(), NUM_GET_BLOCKS as usize); } + #[cfg(not(feature = "get-blocks-disabled"))] + { + let req = GetBlocksRequest { + start: Nat::from(3 * NUM_OPERATIONS), + length: Nat::from(NUM_GET_BLOCKS), + }; + let _p = canbench_rs::bench_scope("get_blocks"); + let blocks_res = get_blocks(req); + assert_eq!(blocks_res.blocks.len(), NUM_GET_BLOCKS as usize); + } upgrade(); }) } From b0ba48fe6e5a453d69e638306becea7135a6841c Mon Sep 17 00:00:00 2001 From: "pr-creation-bot-dfinity-ic[bot]" <200595415+pr-creation-bot-dfinity-ic[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 07:59:34 -0700 Subject: [PATCH 63/75] chore: Update Base Image Refs [2026-06-18-0927] (#10502) Updating base container image references. Run URL: https://github.com/dfinity/ic/actions/runs/27749858613 Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> --- ic-os/bootloader/context/docker-base | 2 +- ic-os/guestos/context/docker-base.dev | 2 +- ic-os/guestos/context/docker-base.prod | 2 +- ic-os/hostos/context/docker-base.dev | 2 +- ic-os/hostos/context/docker-base.prod | 2 +- ic-os/setupos/context/docker-base.dev | 2 +- ic-os/setupos/context/docker-base.prod | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ic-os/bootloader/context/docker-base b/ic-os/bootloader/context/docker-base index cf2db8e15f01..2c3f3ae818f7 100644 --- a/ic-os/bootloader/context/docker-base +++ b/ic-os/bootloader/context/docker-base @@ -1 +1 @@ -ghcr.io/dfinity/bootloader-base@sha256:1c4865120a7f69145a0266091e05e3385246c7ada3bb2d747798a2e4f51735f9 +ghcr.io/dfinity/bootloader-base@sha256:0420913a68c05e825f2b590745b95174c2bbc24e466848d4fc72f899b7345b34 diff --git a/ic-os/guestos/context/docker-base.dev b/ic-os/guestos/context/docker-base.dev index 5d624ee4e8ca..dd614c93672e 100644 --- a/ic-os/guestos/context/docker-base.dev +++ b/ic-os/guestos/context/docker-base.dev @@ -1 +1 @@ -ghcr.io/dfinity/guestos-base-dev@sha256:d6695ef09829496c88e7903f7d5c20e452625f50e8befa2954b6b5f6bc36c8a4 +ghcr.io/dfinity/guestos-base-dev@sha256:71269d8ed5a1b12301855b36ccd86403eed4af3d1c3ebf6e5db02215cc647654 diff --git a/ic-os/guestos/context/docker-base.prod b/ic-os/guestos/context/docker-base.prod index e6b6fff6f9f0..109a9d79df0c 100644 --- a/ic-os/guestos/context/docker-base.prod +++ b/ic-os/guestos/context/docker-base.prod @@ -1 +1 @@ -ghcr.io/dfinity/guestos-base@sha256:ae4b55e9ac8f81a3a8a177d1a22ae24296b68cc6fc12651713c5121f89a596d2 +ghcr.io/dfinity/guestos-base@sha256:3ff0d5fa2c4f2e5c894c0cf61e9a8287d8989a9dd40916d77666b07378beb788 diff --git a/ic-os/hostos/context/docker-base.dev b/ic-os/hostos/context/docker-base.dev index 22160d0434cf..4c0afde802b4 100644 --- a/ic-os/hostos/context/docker-base.dev +++ b/ic-os/hostos/context/docker-base.dev @@ -1 +1 @@ -ghcr.io/dfinity/hostos-base-dev@sha256:056e3249bb98d6351d2bb29f7e8d213ed51ec38b58e17d865647ad612fb3beaa +ghcr.io/dfinity/hostos-base-dev@sha256:801f74ca0387053224695c21446efba538b0e5384bd572fde26d095f2faf59fc diff --git a/ic-os/hostos/context/docker-base.prod b/ic-os/hostos/context/docker-base.prod index dbaaa0694476..2941aae5968e 100644 --- a/ic-os/hostos/context/docker-base.prod +++ b/ic-os/hostos/context/docker-base.prod @@ -1 +1 @@ -ghcr.io/dfinity/hostos-base@sha256:63804a7856c583d776753d361485a71ca7bc314b6c0e6ba42f25dd673935900f +ghcr.io/dfinity/hostos-base@sha256:3fd73db31acc0d22baafd571e2893a8532046aeb920c8fd22cd10951ef1a2ae6 diff --git a/ic-os/setupos/context/docker-base.dev b/ic-os/setupos/context/docker-base.dev index a03f84b9fd8d..e13f8a693cbd 100644 --- a/ic-os/setupos/context/docker-base.dev +++ b/ic-os/setupos/context/docker-base.dev @@ -1 +1 @@ -ghcr.io/dfinity/setupos-base-dev@sha256:bca2fb8755eb1ec78414f69ecf23509e295248f201b26ab6de01faa1092ca3ca +ghcr.io/dfinity/setupos-base-dev@sha256:99e3718d87ac6e0c7ddbd123a65ae946db0ea1f8e69ce630ae1e6fa0b3a9e796 diff --git a/ic-os/setupos/context/docker-base.prod b/ic-os/setupos/context/docker-base.prod index e83cd2379820..353d54c082ad 100644 --- a/ic-os/setupos/context/docker-base.prod +++ b/ic-os/setupos/context/docker-base.prod @@ -1 +1 @@ -ghcr.io/dfinity/setupos-base@sha256:91869212140e01d33b0a9f82cf1ced7e062a822c6fc094578d5b9d8d94e573e7 +ghcr.io/dfinity/setupos-base@sha256:5202c2337e812edc1ced6210a10b5b5e56268f569d078f077a500a9d0efa9995 From 47afd2433bf562ab92cbfdeb2cca0b0eff516232 Mon Sep 17 00:00:00 2001 From: michael-weigelt <122277901+michael-weigelt@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:19:54 +0200 Subject: [PATCH 64/75] chore(Core): Pass through config for DMT (#10507) No semantic changes. All page costs now use the same source. --- rs/embedders/src/wasmtime_embedder.rs | 20 ++++++++++++-------- rs/memory_tracker/benches/traps.rs | 2 ++ rs/memory_tracker/src/deterministic.rs | 15 ++++++++------- rs/memory_tracker/src/lib.rs | 4 ++++ rs/memory_tracker/src/prefetching.rs | 1 + rs/memory_tracker/src/tests.rs | 2 ++ 6 files changed, 29 insertions(+), 15 deletions(-) diff --git a/rs/embedders/src/wasmtime_embedder.rs b/rs/embedders/src/wasmtime_embedder.rs index aa7fb3281ae4..f74097c71a1e 100644 --- a/rs/embedders/src/wasmtime_embedder.rs +++ b/rs/embedders/src/wasmtime_embedder.rs @@ -631,14 +631,6 @@ impl WasmtimeEmbedder { } }; - let memory_trackers = sigsegv_memory_tracker( - memories, - &mut *store, - self.log.clone(), - self.config.feature_flags.deterministic_memory_tracker, - subtract_instruction_counter, - ); - let signal_stack = WasmtimeSignalStack::new(); let mut main_memory_type = WasmMemoryType::Wasm32; if let Some(mem) = instance.get_memory(&mut *store, WASM_HEAP_MEMORY_NAME) @@ -646,6 +638,7 @@ impl WasmtimeEmbedder { { main_memory_type = WasmMemoryType::Wasm64; } + let dirty_page_overhead = match main_memory_type { WasmMemoryType::Wasm32 => self.config.dirty_page_overhead, WasmMemoryType::Wasm64 => NumInstructions::from( @@ -654,6 +647,15 @@ impl WasmtimeEmbedder { ), }; + let memory_trackers = sigsegv_memory_tracker( + memories, + &mut *store, + self.log.clone(), + self.config.feature_flags.deterministic_memory_tracker, + /*dirty_page_overhead*/ NumInstructions::new(1), + subtract_instruction_counter, + ); + Ok(WasmtimeInstance { instance, memory_trackers, @@ -798,6 +800,7 @@ fn sigsegv_memory_tracker( store: &mut wasmtime::Store, log: ReplicaLogger, deterministic_memory_tracker: FlagStatus, + page_overhead: NumInstructions, subtract_instruction_counter: Arc>, ) -> HashMap>> { let maybe_missing_page_handler_kind = match deterministic_memory_tracker { @@ -843,6 +846,7 @@ fn sigsegv_memory_tracker( page_map, maybe_missing_page_handler_kind, memory_limits, + page_overhead.get(), subtract_instruction_counter.clone(), ) .expect("failed to instantiate SIGSEGV memory tracker"), diff --git a/rs/memory_tracker/benches/traps.rs b/rs/memory_tracker/benches/traps.rs index 2f5bc91e5289..890656f409c3 100644 --- a/rs/memory_tracker/benches/traps.rs +++ b/rs/memory_tracker/benches/traps.rs @@ -59,6 +59,7 @@ fn criterion_fault_handler_sim_read(criterion: &mut Criterion) { DirtyPageTracking::Track, page_map.clone(), MemoryLimits::default(), + /* prefetching does not use this anyway */ 0, Arc::new(SignalMutex::new(|_| {})), ) .unwrap(), @@ -109,6 +110,7 @@ fn criterion_fault_handler_sim_write(criterion: &mut Criterion) { DirtyPageTracking::Track, page_map.clone(), MemoryLimits::default(), + /* prefetching does not use this anyway */ 0, Arc::new(SignalMutex::new(|_| {})), ) .unwrap(), diff --git a/rs/memory_tracker/src/deterministic.rs b/rs/memory_tracker/src/deterministic.rs index 691d2f8efab1..3d58ca1a7a64 100644 --- a/rs/memory_tracker/src/deterministic.rs +++ b/rs/memory_tracker/src/deterministic.rs @@ -330,12 +330,13 @@ pub(crate) struct DeterministicMemoryTracker { checksum: RefCell, pub metrics: MemoryTrackerMetrics, state: RefCell, + page_overhead: u64, subtract_instruction_counter: Arc>, } impl DeterministicMemoryTracker { /// Marks a Wasm page as accessed and adds its OS pages to the accessed_pages list. - /// Charges 1 instruction per OS page accessed. + /// Charges instructions per OS page accessed. fn mark_wasm_page_accessed( &self, state: &mut DeterministicState, @@ -344,7 +345,6 @@ impl DeterministicMemoryTracker { state.mark_wasm_page_accessed(wasm_page_idx); // Add the corresponding OS pages to the accessed_pages vector - // and charge 1 instruction per OS page accessed. let os_page_range = Range::from_wasm_page_idx(wasm_page_idx); let mut accessed_pages = self.accessed_pages.borrow_mut(); let num_os_pages = os_page_range.end.get() - os_page_range.start.get(); @@ -352,19 +352,18 @@ impl DeterministicMemoryTracker { accessed_pages.push(PageIndex::new(os_page_idx)); } - // Charge 1 instruction per OS page accessed. - (self.subtract_instruction_counter.lock())(num_os_pages); + // Charge instructions per OS page accessed. + (self.subtract_instruction_counter.lock())(num_os_pages * self.page_overhead); } /// Marks a Wasm page as dirty. - /// Charges 1 instruction per OS page dirtied. + /// Charges instructions per OS page dirtied. fn mark_wasm_page_dirty(&self, state: &mut DeterministicState, wasm_page_idx: WasmPageIndex) { state.mark_wasm_page_dirty(wasm_page_idx); - // Charge 1 instruction per OS page dirtied. let os_page_range = Range::from_wasm_page_idx(wasm_page_idx); let num_os_pages = os_page_range.end.get() - os_page_range.start.get(); - (self.subtract_instruction_counter.lock())(num_os_pages); + (self.subtract_instruction_counter.lock())(num_os_pages * self.page_overhead); } /// A missing OS page handler that provides deterministic prefetching behavior @@ -473,6 +472,7 @@ impl MemoryTracker for DeterministicMemoryTracker { dirty_page_tracking: DirtyPageTracking, page_map: PageMap, memory_limits: MemoryLimits, + page_overhead: u64, subtract_instruction_counter: Arc>, ) -> nix::Result where @@ -506,6 +506,7 @@ impl MemoryTracker for DeterministicMemoryTracker { checksum: RefCell::new(checksum::SigsegChecksum::default()), metrics: MemoryTrackerMetrics::default(), state: RefCell::new(state), + page_overhead, subtract_instruction_counter, }; diff --git a/rs/memory_tracker/src/lib.rs b/rs/memory_tracker/src/lib.rs index 2c812006e6d4..a7f385d1b035 100644 --- a/rs/memory_tracker/src/lib.rs +++ b/rs/memory_tracker/src/lib.rs @@ -384,6 +384,7 @@ pub trait MemoryTracker { dirty_page_tracking: DirtyPageTracking, page_map: PageMap, memory_limits: MemoryLimits, + page_overhead: u64, subtract_instruction_counter: Arc>, ) -> nix::Result where @@ -440,6 +441,7 @@ pub fn new( page_map: PageMap, missing_page_handler_kind: Option, memory_limits: MemoryLimits, + page_overhead: u64, subtract_instruction_counter: Arc>, ) -> nix::Result { match missing_page_handler_kind { @@ -451,6 +453,7 @@ pub fn new( dirty_page_tracking, page_map, memory_limits, + page_overhead, subtract_instruction_counter, )?)) } @@ -461,6 +464,7 @@ pub fn new( dirty_page_tracking, page_map, memory_limits, + page_overhead, subtract_instruction_counter, )?)), } diff --git a/rs/memory_tracker/src/prefetching.rs b/rs/memory_tracker/src/prefetching.rs index 66c16637e430..db9a239ecc38 100644 --- a/rs/memory_tracker/src/prefetching.rs +++ b/rs/memory_tracker/src/prefetching.rs @@ -60,6 +60,7 @@ impl MemoryTracker for PrefetchingMemoryTracker { dirty_page_tracking: DirtyPageTracking, page_map: PageMap, _memory_limits: MemoryLimits, + _page_overhead: u64, _subtract_instruction_counter: Arc>, ) -> nix::Result where diff --git a/rs/memory_tracker/src/tests.rs b/rs/memory_tracker/src/tests.rs index 84b36e13e3a2..dcbc5fa2ef54 100644 --- a/rs/memory_tracker/src/tests.rs +++ b/rs/memory_tracker/src/tests.rs @@ -86,6 +86,7 @@ fn setup( max_memory_size: NumBytes::new((memory_pages * PAGE_SIZE) as u64), max_dirty_pages: NumOsPages::new(memory_pages as u64), }, + /* page_overhead not relevant in these tests */ 1, Arc::new(SignalMutex::new(|_| {})), ) .unwrap(), @@ -104,6 +105,7 @@ fn setup( max_memory_size: NumBytes::new((memory_pages * PAGE_SIZE) as u64), max_dirty_pages: NumOsPages::new(memory_pages as u64), }, + /* prefetching does not use this anyway */ 0, Arc::new(SignalMutex::new(|_| {})), ) .unwrap(), From 813418fa4b12be642cd38c0c94bb500d6f368db2 Mon Sep 17 00:00:00 2001 From: michael-weigelt <122277901+michael-weigelt@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:29:43 +0200 Subject: [PATCH 65/75] chore(Core): Remove unnecessary heartbeats in tests (#10508) --- rs/execution_environment/tests/dts.rs | 66 ++++++++++++--------------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/rs/execution_environment/tests/dts.rs b/rs/execution_environment/tests/dts.rs index 3c53ee0d28e8..e35baa833eff 100644 --- a/rs/execution_environment/tests/dts.rs +++ b/rs/execution_environment/tests/dts.rs @@ -559,7 +559,7 @@ fn dts_pending_upgrade_with_heartbeat() { let controller = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -644,7 +644,7 @@ fn dts_scheduling_of_install_code() { let controller = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -798,7 +798,7 @@ fn dts_pending_install_code_does_not_block_subnet_messages_of_other_canisters() for _ in 0..n { let id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -985,7 +985,7 @@ fn dts_aborted_execution_does_not_block_subnet_messages() { let user_id = PrincipalId::new_anonymous(); let other_canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -993,7 +993,7 @@ fn dts_aborted_execution_does_not_block_subnet_messages() { .unwrap(); let aborted_canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], Some( CanisterSettingsArgsBuilder::new() @@ -1090,7 +1090,7 @@ fn dts_paused_execution_blocks_deposit_cycles() { let user_id = PrincipalId::new_anonymous(); let long_canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1098,7 +1098,7 @@ fn dts_paused_execution_blocks_deposit_cycles() { .unwrap(); let other_canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1242,7 +1242,7 @@ fn dts_long_running_install_and_update() { for _ in 0..n { let id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1262,7 +1262,7 @@ fn dts_long_running_install_and_update() { let id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], settings.clone(), INITIAL_CYCLES_BALANCE, @@ -1273,7 +1273,7 @@ fn dts_long_running_install_and_update() { let short = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1285,7 +1285,7 @@ fn dts_long_running_install_and_update() { let args = InstallCodeArgs::new( CanisterInstallMode::Upgrade, canister[i], - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], ); let payload = wasm() @@ -1368,7 +1368,7 @@ fn dts_long_running_calls() { for _ in 0..n { let id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1379,7 +1379,7 @@ fn dts_long_running_calls() { let short = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1713,7 +1713,7 @@ fn dts_ingress_status_of_update_with_call_is_correct() { NumInstructions::from(10_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let user_id = PrincipalId::new_anonymous(); @@ -1933,7 +1933,7 @@ fn dts_serialized_and_runtime_states_are_equal() { for _ in 0..num_canisters { let canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -2241,26 +2241,23 @@ fn dts_global_timer_one_shot_works() { NumInstructions::from(50_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) .unwrap(); - // 1) canister create and 2) install code. - assert_eq!(2, get_canister_version(&env, canister_id)); + // 1) install code. + assert_eq!(1, get_canister_version(&env, canister_id)); let now_nanos = env.time().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64; - let disable_heartbeats = wasm().trap().build(); - let timer = wasm() .instruction_counter_is_at_least(100_000) .inc_global_counter() .build(); let set_heartbeat_and_global_timer = wasm() - .set_heartbeat(disable_heartbeats) .set_global_timer_method(timer) .api_global_timer_set(now_nanos) .get_global_counter() @@ -2273,9 +2270,9 @@ fn dts_global_timer_one_shot_works() { assert_eq!(result, WasmResult::Reply(0_u64.to_le_bytes().to_vec())); - // 3) the update. + // 2) the update. let base_canister_version = get_canister_version(&env, canister_id); - assert_eq!(3, base_canister_version); + assert_eq!(2, base_canister_version); for i in 1..10 { env.tick(); @@ -2388,7 +2385,7 @@ fn dts_global_timer_resume_after_abort() { NumInstructions::from(60_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) @@ -2438,7 +2435,7 @@ fn dts_global_timer_does_not_prevent_canister_from_stopping() { NumInstructions::from(60_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) @@ -2486,19 +2483,17 @@ fn dts_global_timer_with_trap() { NumInstructions::from(50_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) .unwrap(); - // 1) canister create and 2) install code. - assert_eq!(2, get_canister_version(&env, canister_id)); + // 1) install code. + assert_eq!(1, get_canister_version(&env, canister_id)); let now_nanos = env.time().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64; - let disable_heartbeats = wasm().trap().build(); - let timer = wasm() .instruction_counter_is_at_least(100_000) .inc_global_counter() @@ -2507,7 +2502,6 @@ fn dts_global_timer_with_trap() { .build(); let set_heartbeat_and_global_timer = wasm() - .set_heartbeat(disable_heartbeats) .set_global_timer_method(timer) .api_global_timer_set(now_nanos) .get_global_counter() @@ -2520,9 +2514,9 @@ fn dts_global_timer_with_trap() { assert_eq!(result, WasmResult::Reply(0_u64.to_le_bytes().to_vec())); - // 3) the update. + // 2) the update. let base_canister_version = get_canister_version(&env, canister_id); - assert_eq!(3, base_canister_version); + assert_eq!(2, base_canister_version); for _ in 1..10 { env.tick(); @@ -2541,7 +2535,7 @@ fn dts_global_timer_does_not_prevent_upgrade() { NumInstructions::from(60_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) @@ -2589,7 +2583,7 @@ fn dts_abort_paused_execution_on_state_switch() { ); let user_id = PrincipalId::new_anonymous(); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) @@ -2828,7 +2822,7 @@ fn heavy_install_code_prevents_another_install_code_to_start_in_the_same_round() InstallCodeArgs::new( CanisterInstallMode::Reinstall, canister_id, - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), canister_init, ) .encode(), From 6e48d353fa6df8b80bdb5f26888eaa125407f2e3 Mon Sep 17 00:00:00 2001 From: Pierugo Pace Date: Fri, 19 Jun 2026 11:00:39 +0200 Subject: [PATCH 66/75] fix: deflake //rs/consensus:cup_explorer_test (#10515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `:cup_explorer_test` recently flaked with `Attempts to run this DKG repeatedly failed`. This happened to two runs already (on June 18th and on June 11th). Since this could be related to the recent DKG changes, I spent quite a bit of time to understand if there was a real regression, but it looks like the subnet genuinely does not have time to create the transcripts in one interval when CI is under load. The following is Claude's opinion, but @eichhorl you might want to take a closer look, here's the BuildBuddy [link](https://dash.dm1-idx1.dfinity.network/invocation/ebaae5b9-7402-4a0b-b827-5b662b81d096?target=%2F%2Frs%2Ftests%2Fconsensus%3Acup_explorer_test&targetStatus=11). Why the DKG timed out — a timing/load race: The NNS is `Subnet::fast_single_node(...)` with `DKG_INTERVAL = 14`. It produces a new DKG summary every ~3.5s (failed run: heights 300→315→330→345→360 at 15:01:36→…→15:01:50). The recovery context appears at ~15:01:34.8, so it gets only ~5 fast intervals (~14–17s of wall-clock) to complete, while each interval already spends ~1.95s just loading the low‑threshold transcript on the single overloaded node. **Failed run**: never completed in those 5 attempts. **Passed run**: the same DKG finished in ~1 interval (transcripts found at height 296, ~12s after the recover call). Purely a wall-clock-per-attempt race. --- rs/tests/consensus/cup_explorer_test.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/rs/tests/consensus/cup_explorer_test.rs b/rs/tests/consensus/cup_explorer_test.rs index c2b7eb32d61c..7dcf0e234036 100644 --- a/rs/tests/consensus/cup_explorer_test.rs +++ b/rs/tests/consensus/cup_explorer_test.rs @@ -54,10 +54,7 @@ const NODES_COUNT: usize = 4; fn setup(env: TestEnv) { InternetComputer::new() - .add_subnet( - Subnet::fast_single_node(SubnetType::System) - .with_dkg_interval_length(Height::from(DKG_INTERVAL)), - ) + .add_subnet(Subnet::fast_single_node(SubnetType::System)) .add_subnet( Subnet::new(SubnetType::Application) .with_dkg_interval_length(Height::from(DKG_INTERVAL)) @@ -171,7 +168,7 @@ fn test(env: TestEnv) { ); let update_subnet_payload = UpdateSubnetPayload { subnet_id: app_subnet.subnet_id, - dkg_interval_length: Some(14), + dkg_interval_length: Some(DKG_INTERVAL), subnet_admins: None, ..empty_subnet_update() }; From 052e61d0872eb7d944fbd7717082415b21eb040a Mon Sep 17 00:00:00 2001 From: Jan Wendling <7381150+jwndlng@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:56:08 +0200 Subject: [PATCH 67/75] chore: revise vulnerability disclosure process in SECURITY.md (#10516) Updates the section on how to report a vulnerability by removing the securitybugs@dfinity.org mail address. --- SECURITY.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 865701d1180b..5ae7a3d81c0e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,13 +11,8 @@ We appreciate your help in keeping our projects secure. If you believe you have found a security vulnerability in any of our repositories, please report it responsibly to us as described below: 1. **Do not disclose the vulnerability publicly.** Public disclosure could be exploited by attackers before it can be fixed. -2. **Ideally, disclose the vulnerability through [Hackenproof](https://hackenproof.com/programs/internet-computer-protocol)** +2. **Disclose the vulnerability through [Hackenproof](https://hackenproof.com/programs/internet-computer-protocol)** * Hackenproof facilitates disclosure and streamlines Bugbounty payouts. -4. **Alternatively, send an email to securitybugs@dfinity.org.** Please include the following information in your email: - * A description of the vulnerability - * Steps to reproduce the vulnerability - * Risk rating of the vulnerability - * Any other relevant information We will respond to your report within 72 hours and work with you to fix the vulnerability as soon as possible. From a547cc34569059d3887758d7e452ef3b10fcd896 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn <99166915+eichhorl@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:08:45 +0200 Subject: [PATCH 68/75] feat(https-outcalls): Introduce pay-as-you-go base fee (#10501) ## Background Currently, HTTPS outcalls are charged upfront based on a `max_response_bytes` parameter. This is done by subtracting the full cost from the caller's payment. The remaining cycles are stored in the request context and refunded once a response is delivered. Instead, we want to introduce pay-as-you-go pricing, which charges cycles whenever resources are consumed. This happens in three stages: 1. Base cost. This fee is charged for every request upfront 2. Per-replica cost. The remaining cycles after charging the base cost are split evenly between the participating replicas. Each replica consumes some of their allowance as the HTTP request is processed. In the end, the remaining cycles are gossiped as part of refund shares. 3. Consensus cost. This fee is charged for including the aggregated HTTP response as part of a block. The cost is covered by the sum of refund shares that were included in the aggregated response. Cycles remaining after charging the consensus cost are refunded to the user asynchronously. ## Proposed Changes This PR introduces a function to calculate the base cost (1.) based on the request context and the subnet's cycles cost config. The pricing formula is defined [here](https://colab.research.google.com/drive/1MyZBivzU_5kQtqQ8Z71gYbrMubGrTXmv#scrollTo=vxa3d-NYyEOi) (internal). Unlike legacy pricing, for pay-as-you-go pricing, the user's _entire_ payment is taken out of the request. This means nothing will be refunded when a response is returned. The asynchronous refunds will instead take a different path. After charging the base cost, the payment is used to populate the context's `RefundStatus` which determines the per-replica allowance as described above. The exception is if the subnet's cycle cost schedule is `Free`, in which case the caller's payment in the request remains untouched, and no refundable cycles are generated. All of the attached cycles are returned back to the caller when a response is delivered. Additionally, we introduce a (disabled) feature flag for flexible outcalls, in order to test the changes end-to-end. The legacy charging flow should be unchanged by this PR. --------- Co-authored-by: Claude Opus 4.8 (1M context) --- rs/config/src/execution_environment.rs | 6 + rs/config/src/subnet_config.rs | 10 + .../src/cycles_account_manager.rs | 50 ++- .../src/execution_environment.rs | 217 +++++++--- .../src/execution_environment/tests.rs | 401 +++++++++++++++++- .../execution_environment/src/lib.rs | 18 + .../networking/canister_http_flexible_test.rs | 2 +- rs/types/types/src/canister_http.rs | 24 +- 8 files changed, 643 insertions(+), 85 deletions(-) diff --git a/rs/config/src/execution_environment.rs b/rs/config/src/execution_environment.rs index 75a72fc8aacc..7d5d81a2cfa5 100644 --- a/rs/config/src/execution_environment.rs +++ b/rs/config/src/execution_environment.rs @@ -13,6 +13,8 @@ const TIB: u64 = 1024 * GIB; const REPLICATED_INTER_CANISTER_LOG_FETCH_FEATURE: FlagStatus = FlagStatus::Disabled; +const FLEXIBLE_HTTP_REQUESTS_FEATURE: FlagStatus = FlagStatus::Disabled; + // TODO(DSM-105): remove after the feature is enabled by default. pub const LOG_MEMORY_STORE_FEATURE_ENABLED: bool = false; pub const LOG_MEMORY_STORE_FEATURE: FlagStatus = if LOG_MEMORY_STORE_FEATURE_ENABLED { @@ -381,6 +383,9 @@ pub struct Config { /// Enables the log memory store feature. pub log_memory_store_feature: FlagStatus, + + /// Enables the flexible HTTP outcalls API (`flexible_http_request`). + pub flexible_http_requests: FlagStatus, } impl Default for Config { @@ -467,6 +472,7 @@ impl Default for Config { max_environment_variable_value_length: MAX_ENVIRONMENT_VARIABLE_VALUE_LENGTH, replicated_inter_canister_log_fetch: REPLICATED_INTER_CANISTER_LOG_FETCH_FEATURE, log_memory_store_feature: LOG_MEMORY_STORE_FEATURE, + flexible_http_requests: FLEXIBLE_HTTP_REQUESTS_FEATURE, } } } diff --git a/rs/config/src/subnet_config.rs b/rs/config/src/subnet_config.rs index 14d51e72f421..9e5021d0d01c 100644 --- a/rs/config/src/subnet_config.rs +++ b/rs/config/src/subnet_config.rs @@ -145,6 +145,16 @@ pub const SCHNORR_SIGNATURE_FEE: Cycles = Cycles::new(10 * B as u128); /// cover the cost of the subnet. pub const VETKD_FEE: Cycles = Cycles::new(10 * B as u128); +/// Pay-as-you-go base-fee pricing constants for HTTP outcalls, charged upfront +/// for every request by `CyclesAccountManager::http_request_base_fee`. +pub const HTTP_REQUEST_BASE_FEE: u128 = 1_000_000; +pub const HTTP_REQUEST_PER_BYTE_FEE: u128 = 50; +pub const HTTP_REQUEST_FULLY_REPLICATED_PER_NODE_FEE: u128 = 140_000; +pub const HTTP_REQUEST_FULLY_REPLICATED_QUADRATIC_NODE_FEE: u128 = 800; +pub const HTTP_REQUEST_FLEXIBLE_PER_NODE_FEE: u128 = 90_000; +pub const HTTP_REQUEST_FLEXIBLE_PER_NODE_RESPONSE_CONSENSUS_FEE: u128 = 2_000; +pub const HTTP_REQUEST_FLEXIBLE_PER_RESPONSE_CONSENSUS_FEE: u128 = 100_000; + /// Default subnet size which is used to scale cycles cost according to a subnet replication factor. /// /// All initial costs were calculated with the assumption that a subnet had 13 replicas. diff --git a/rs/cycles_account_manager/src/cycles_account_manager.rs b/rs/cycles_account_manager/src/cycles_account_manager.rs index afb10e06c0ab..067d7adf3511 100644 --- a/rs/cycles_account_manager/src/cycles_account_manager.rs +++ b/rs/cycles_account_manager/src/cycles_account_manager.rs @@ -1,6 +1,11 @@ use super::{CRITICAL_ERROR_EXECUTION_CYCLES_REFUND, CRITICAL_ERROR_RESPONSE_CYCLES_REFUND}; use ic_base_types::NumSeconds; -use ic_config::subnet_config::CyclesAccountManagerConfig; +use ic_config::subnet_config::{ + CyclesAccountManagerConfig, HTTP_REQUEST_BASE_FEE, HTTP_REQUEST_FLEXIBLE_PER_NODE_FEE, + HTTP_REQUEST_FLEXIBLE_PER_NODE_RESPONSE_CONSENSUS_FEE, + HTTP_REQUEST_FLEXIBLE_PER_RESPONSE_CONSENSUS_FEE, HTTP_REQUEST_FULLY_REPLICATED_PER_NODE_FEE, + HTTP_REQUEST_FULLY_REPLICATED_QUADRATIC_NODE_FEE, HTTP_REQUEST_PER_BYTE_FEE, +}; use ic_interfaces::execution_environment::{CanisterOutOfCyclesError, MessageMemoryUsage}; use ic_logger::{ReplicaLogger, error, info}; use ic_management_canister_types_private::Method; @@ -12,7 +17,7 @@ use ic_replicated_state::{ use ic_types::{ CanisterId, ComputeAllocation, MemoryAllocation, NumBytes, NumInstructions, PrincipalId, SubnetId, - canister_http::MAX_CANISTER_HTTP_RESPONSE_BYTES, + canister_http::{MAX_CANISTER_HTTP_RESPONSE_BYTES, Replication}, canister_log::MAX_FETCH_CANISTER_LOGS_RESPONSE_BYTES, messages::{MAX_INTER_CANISTER_PAYLOAD_IN_BYTES, Payload, SignedIngress}, }; @@ -1236,6 +1241,47 @@ impl CyclesAccountManager { CompoundCycles::new(amount, subnet_cycles_config.cost_schedule) } + pub fn http_request_base_fee( + &self, + request_size: NumBytes, + replication: &Replication, + subnet_cycles_config: CyclesAccountManagerSubnetConfig, + ) -> CompoundCycles { + let n = subnet_cycles_config.subnet_size as u128; + let request_bytes = request_size.get() as u128; + let per_replica = match replication { + Replication::FullyReplicated => { + HTTP_REQUEST_BASE_FEE + + HTTP_REQUEST_PER_BYTE_FEE * request_bytes + + HTTP_REQUEST_FULLY_REPLICATED_PER_NODE_FEE * n + + HTTP_REQUEST_FULLY_REPLICATED_QUADRATIC_NODE_FEE * n * n + } + Replication::Flexible { + min_responses: min, .. + } => { + let min = *min as u128; + HTTP_REQUEST_BASE_FEE + + HTTP_REQUEST_PER_BYTE_FEE * request_bytes + + HTTP_REQUEST_FLEXIBLE_PER_NODE_FEE * n + + HTTP_REQUEST_FLEXIBLE_PER_NODE_RESPONSE_CONSENSUS_FEE * n * min + + HTTP_REQUEST_FLEXIBLE_PER_RESPONSE_CONSENSUS_FEE * min + } + Replication::NonReplicated(_) => { + // Non-replicated is equivalent to flexible replication with min_responses = 1. + HTTP_REQUEST_BASE_FEE + + HTTP_REQUEST_PER_BYTE_FEE * request_bytes + + HTTP_REQUEST_FLEXIBLE_PER_NODE_FEE * n + + HTTP_REQUEST_FLEXIBLE_PER_NODE_RESPONSE_CONSENSUS_FEE * n + + HTTP_REQUEST_FLEXIBLE_PER_RESPONSE_CONSENSUS_FEE + } + }; + + CompoundCycles::new( + Cycles::new(n * per_replica), + subnet_cycles_config.cost_schedule, + ) + } + pub fn http_request_fee_v2( &self, request_size: NumBytes, diff --git a/rs/execution_environment/src/execution_environment.rs b/rs/execution_environment/src/execution_environment.rs index e947f3e0c1ba..bbc1484bede5 100644 --- a/rs/execution_environment/src/execution_environment.rs +++ b/rs/execution_environment/src/execution_environment.rs @@ -64,7 +64,10 @@ use ic_replicated_state::{ CanisterState, CanisterStatus, ExecutionTask, NetworkTopology, ReplicatedState, }; use ic_types::batch::ChainKeyData; -use ic_types::canister_http::{CanisterHttpRequestContext, MAX_CANISTER_HTTP_RESPONSE_BYTES}; +use ic_types::canister_http::{ + CanisterHttpRequestContext, MAX_CANISTER_HTTP_RESPONSE_BYTES, PricingVersion, RefundStatus, + Replication, +}; use ic_types::consensus::idkg::IDkgMasterPublicKeyId; use ic_types::crypto::{ ExtendedDerivationPath, @@ -1214,25 +1217,54 @@ impl ExecutionEnvironment { } }, - Ok(Ic00Method::FlexibleHttpRequest) => match &msg { - CanisterCall::Request(_) => { - match FlexibleCanisterHttpRequestArgs::decode(payload) { - Err(err) => ExecuteSubnetMessageResult::Finished { - response: Err(err), - refund: msg.take_cycles(), - }, - Ok(_) => ExecuteSubnetMessageResult::Finished { - response: Err(UserError::new( - ErrorCode::CanisterRejectedMessage, - "FlexibleHttpRequest is not yet implemented".to_string(), - )), - refund: msg.take_cycles(), - }, + Ok(Ic00Method::FlexibleHttpRequest) => match self.config.flexible_http_requests { + FlagStatus::Disabled => ExecuteSubnetMessageResult::Finished { + response: Err(UserError::new( + ErrorCode::CanisterContractViolation, + "This API is not enabled on this subnet".to_string(), + )), + refund: msg.take_cycles(), + }, + FlagStatus::Enabled => match &msg { + CanisterCall::Request(request) => { + match FlexibleCanisterHttpRequestArgs::decode(payload) { + Err(err) => ExecuteSubnetMessageResult::Finished { + response: Err(err), + refund: msg.take_cycles(), + }, + Ok(args) => { + match CanisterHttpRequestContext::generate_from_flexible_args( + state.time(), + request.as_ref(), + args, + ®istry_settings.node_ids, + rng, + ) { + Err(err) => ExecuteSubnetMessageResult::Finished { + response: Err(err.into()), + refund: msg.take_cycles(), + }, + Ok(canister_http_request_context) => match self + .try_add_http_context_to_replicated_state( + canister_http_request_context, + &mut state, + request.as_ref(), + since, + ) { + Err(err) => ExecuteSubnetMessageResult::Finished { + response: Err(err), + refund: msg.take_cycles(), + }, + Ok(()) => ExecuteSubnetMessageResult::Processing, + }, + } + } + } } - } - CanisterCall::Ingress(_) => { - self.reject_unexpected_ingress(Ic00Method::FlexibleHttpRequest) - } + CanisterCall::Ingress(_) => { + self.reject_unexpected_ingress(Ic00Method::FlexibleHttpRequest) + } + }, }, Ok(Ic00Method::HttpRequest) => match state.metadata.own_subnet_features.http_requests { @@ -2087,13 +2119,24 @@ impl ExecutionEnvironment { request: &Request, since: Instant, ) -> Result<(), UserError> { - let http_request_fee = self.cycles_account_manager.http_request_fee( - canister_http_request_context.variable_parts_size(), + let variable_parts_size = canister_http_request_context.variable_parts_size(); + let cycles_config = state.get_own_subnet_cycles_config(); + let cost_schedule = cycles_config.cost_schedule; + let legacy_fee = self.cycles_account_manager.http_request_fee( + variable_parts_size, canister_http_request_context.max_response_bytes, - state.get_own_subnet_cycles_config(), + cycles_config, ); - let real_http_request_fee = http_request_fee.real(); - let nominal_http_request_fee = http_request_fee.nominal(); + + // The base fee is the non-refundable part of the payment under + // pay-as-you-go pricing; under legacy pricing the full legacy fee is + // charged instead. + let base_fee = self.cycles_account_manager.http_request_base_fee( + variable_parts_size, + &canister_http_request_context.replication, + cycles_config, + ); + // Here we make sure that we do not let upper layers open new // http calls while the maximum number of calls is in-flight. // Later, in the http adapter we also have a bounded queue of @@ -2109,57 +2152,109 @@ impl ExecutionEnvironment { .len() >= self.config.max_canister_http_requests_in_flight { - Err(UserError::new( + return Err(UserError::new( ErrorCode::CanisterRejectedMessage, format!( "max number ({}) of http requests in-flight reached.", self.config.max_canister_http_requests_in_flight ), - )) - } else if request.payment < real_http_request_fee { - Err(UserError::new( + )); + } + + // The cycles charged upfront depend on the pricing version: legacy + // charges the full request fee, whereas pay-as-you-go charges the base + // fee and refunds the remainder based on the resources actually + // consumed. + let charged_fee = match canister_http_request_context.pricing_version { + PricingVersion::Legacy => legacy_fee, + PricingVersion::PayAsYouGo => base_fee, + }; + if request.payment < charged_fee.real() { + return Err(UserError::new( ErrorCode::CanisterRejectedMessage, format!( "{} request sent with {} cycles, but {} cycles are required.", - Ic00Method::HttpRequest, + request.method_name, request.payment, - real_http_request_fee + charged_fee.real() ), - )) + )); + } + + // The refundable cycles are everything the payment covers beyond the + // base fee; on a free cost schedule nothing is charged, so nothing is + // refundable. We set the refund status even for legacy pricing in order + // to enable observability during the dark launch. However, nothing will + // actually be refunded for legacy pricing. + let refundable_cycles = if cost_schedule == CanisterCyclesCostSchedule::Free { + Cycles::new(0) } else { - canister_http_request_context.request.payment -= real_http_request_fee; - state - .metadata - .subnet_metrics - .observe_consumed_cycles_http_outcalls(nominal_http_request_fee); - state - .metadata - .subnet_metrics - .observe_consumed_cycles_with_use_case( - CyclesUseCase::HTTPOutcalls, - nominal_http_request_fee, - ); - state.metadata.subnet_call_context_manager.push_context( - SubnetCallContext::CanisterHttpRequest(canister_http_request_context), - ); - if let Some(canister_state) = state.canister_state_make_mut(&request.sender) { - canister_state - .system_state - .observe_consumed_cycles_for_https_outcall(nominal_http_request_fee); - canister_state - .system_state - .canister_metrics_mut() - .load_metrics_mut() - .observe_http_outcall(); + canister_http_request_context.request.payment - base_fee.real() + }; + let node_count = match &canister_http_request_context.replication { + Replication::Flexible { committee, .. } => committee.len().max(1), + Replication::NonReplicated(_) => 1, + Replication::FullyReplicated => cycles_config.subnet_size.max(1), + }; + canister_http_request_context.refund_status = RefundStatus { + refundable_cycles, + per_replica_allowance: refundable_cycles / node_count, + refunded_cycles: Cycles::new(0), + refunding_nodes: BTreeSet::new(), + }; + + // The payment deduction differs per pricing version. + match canister_http_request_context.pricing_version { + PricingVersion::Legacy => { + // Legacy pricing deducts the full request fee from the payment. + // The remaining payment is refunded when the response is delivered. + canister_http_request_context.request.payment -= legacy_fee.real(); } - self.metrics.observe_message_with_label( - &request.method_name, - since.elapsed().as_secs_f64(), - SUBMITTED_OUTCOME_LABEL.into(), - SUCCESS_STATUS_LABEL.into(), + PricingVersion::PayAsYouGo => { + // Take out the entire payment upfront; the refundable portion is + // returned later via the refund mechanism. On a free cost + // schedule there is nothing to charge. + if cost_schedule != CanisterCyclesCostSchedule::Free { + canister_http_request_context.request.payment.take(); + } + } + } + + // Observe the nominal cycles charged for this outcall, based on what was + // actually charged (regardless of whether a real charge happens, e.g. on + // a free cost schedule). + let nominal_consumed_cycles = charged_fee.nominal(); + state + .metadata + .subnet_metrics + .observe_consumed_cycles_http_outcalls(nominal_consumed_cycles); + state + .metadata + .subnet_metrics + .observe_consumed_cycles_with_use_case( + CyclesUseCase::HTTPOutcalls, + nominal_consumed_cycles, ); - Ok(()) + state.metadata.subnet_call_context_manager.push_context( + SubnetCallContext::CanisterHttpRequest(canister_http_request_context), + ); + if let Some(canister_state) = state.canister_state_make_mut(&request.sender) { + canister_state + .system_state + .observe_consumed_cycles_for_https_outcall(nominal_consumed_cycles); + canister_state + .system_state + .canister_metrics_mut() + .load_metrics_mut() + .observe_http_outcall(); } + self.metrics.observe_message_with_label( + &request.method_name, + since.elapsed().as_secs_f64(), + SUBMITTED_OUTCOME_LABEL.into(), + SUCCESS_STATUS_LABEL.into(), + ); + Ok(()) } /// Observes a subnet message metrics and outputs the given subnet response. diff --git a/rs/execution_environment/src/execution_environment/tests.rs b/rs/execution_environment/src/execution_environment/tests.rs index abab616628f5..7c3ef06848a1 100644 --- a/rs/execution_environment/src/execution_environment/tests.rs +++ b/rs/execution_environment/src/execution_environment/tests.rs @@ -8,10 +8,11 @@ use ic_management_canister_types_private::{ self as ic00, BitcoinGetUtxosArgs, BoundedHttpHeaders, CanisterChange, CanisterHttpRequestArgs, CanisterIdRecord, CanisterMetadataRequest, CanisterMetadataResponse, CanisterStatusResultV2, CanisterStatusType, CreateCanisterArgs, DerivationPath, EcdsaCurve, EcdsaKeyId, EmptyBlob, - FetchCanisterLogsRequest, HttpMethod, IC_00, LogVisibilityV2, MasterPublicKeyId, Method, - Payload as Ic00Payload, ProvisionalCreateCanisterWithCyclesArgs, ProvisionalTopUpCanisterArgs, - SchnorrAlgorithm, SchnorrKeyId, TakeCanisterSnapshotArgs, TransformContext, TransformFunc, - UploadChunkArgs, VetKdCurve, VetKdKeyId, + FetchCanisterLogsRequest, FlexibleCanisterHttpRequestArgs, HttpMethod, IC_00, LogVisibilityV2, + MasterPublicKeyId, Method, Payload as Ic00Payload, ProvisionalCreateCanisterWithCyclesArgs, + ProvisionalTopUpCanisterArgs, ReplicationCounts, SchnorrAlgorithm, SchnorrKeyId, + TakeCanisterSnapshotArgs, TransformContext, TransformFunc, UploadChunkArgs, VetKdCurve, + VetKdKeyId, }; use ic_registry_routing_table::{CanisterIdRange, RoutingTable, canister_id_into_u64}; use ic_registry_subnet_type::SubnetType; @@ -31,7 +32,7 @@ use ic_test_utilities_execution_environment::{ use ic_test_utilities_metrics::{fetch_histogram_vec_count, metric_vec}; use ic_types::{ CanisterId, CountBytes, PrincipalId, RegistryVersion, - canister_http::{CanisterHttpMethod, Transform}, + canister_http::{CanisterHttpMethod, PricingVersion, Replication, Transform}, consensus::idkg::{IDkgMasterPublicKeyId, PreSigId}, ingress::{IngressState, IngressStatus, WasmResult}, messages::{ @@ -3265,6 +3266,41 @@ fn execute_canister_http_request() { ); assert_eq!(http_request_context.request.payment, payment - fee.real()); + // Legacy pricing populates the refund status from the base fee: the + // refundable cycles are everything beyond the base fee (zero on a free + // cost schedule), split across the refunding nodes (the whole subnet for + // a fully replicated request). + assert_eq!( + http_request_context.replication, + Replication::FullyReplicated + ); + let base_fee = test.http_request_base_fee( + http_request_context.variable_parts_size(), + &http_request_context.replication, + ); + let expected_refundable = match cost_schedule { + CanisterCyclesCostSchedule::Free => Cycles::new(0), + CanisterCyclesCostSchedule::Normal => payment - base_fee.real(), + }; + assert_eq!( + http_request_context.refund_status.refundable_cycles, + expected_refundable + ); + assert_eq!( + http_request_context.refund_status.per_replica_allowance, + expected_refundable / test.subnet_size().max(1) + ); + assert_eq!( + http_request_context.refund_status.refunded_cycles, + Cycles::new(0) + ); + assert!( + http_request_context + .refund_status + .refunding_nodes + .is_empty() + ); + assert_eq!( fee.nominal(), test.state() @@ -3338,6 +3374,361 @@ fn execute_canister_http_request_disabled() { assert_eq!(canister_http_request_contexts.len(), 0); } +#[test] +fn execute_canister_http_request_insufficient_payment() { + // Under legacy pricing the *full* request fee is charged upfront, not just + // the (smaller) base fee. A payment that covers the base fee but not the + // full legacy fee must therefore still be rejected. This pins the legacy + // threshold to the legacy fee and guards against it being accidentally + // lowered to the base fee. + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(10); + let legacy_http_request_args = || CanisterHttpRequestArgs { + url: "https://example.com".to_string(), + // A large response limit makes the legacy fee (which has a response-size + // term) strictly exceed the base fee (which has none). + max_response_bytes: Some(1_000_000), + headers: BoundedHttpHeaders::new(vec![]), + body: None, + method: HttpMethod::GET, + transform: Some(TransformContext { + function: TransformFunc(candid::Func { + principal: caller_canister.get().0, + method: "transform".to_string(), + }), + context: vec![0, 1, 2], + }), + is_replicated: None, + pricing_version: None, + }; + let build_test = || { + ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_caller(own_subnet, caller_canister) + .build() + }; + + // Probe with ample payment to learn the base and legacy fees for these args. + let (base_fee_real, legacy_fee_real) = { + let mut probe = build_test(); + probe.inject_call_to_ic00( + Method::HttpRequest, + legacy_http_request_args().encode(), + Cycles::new(100_000_000_000), + ); + probe.execute_all(); + let context = probe + .state() + .metadata + .subnet_call_context_manager + .canister_http_request_contexts + .get(&CallbackId::from(0)) + .unwrap(); + let size = context.variable_parts_size(); + let base_fee = probe.http_request_base_fee(size, &context.replication); + let legacy_fee = probe.http_request_fee(size, context.max_response_bytes); + (base_fee.real(), legacy_fee.real()) + }; + // The test is only meaningful if the legacy fee strictly exceeds the base + // fee, so that a payment equal to the base fee discriminates between the two + // thresholds. + assert!(base_fee_real < legacy_fee_real); + + // A payment equal to the base fee covers the base fee but not the legacy + // fee, so legacy pricing must reject it without adding a context. + let mut test = build_test(); + test.inject_call_to_ic00( + Method::HttpRequest, + legacy_http_request_args().encode(), + base_fee_real, + ); + test.execute_all(); + assert_eq!( + test.state() + .metadata + .subnet_call_context_manager + .canister_http_request_contexts + .len(), + 0 + ); + assert!(get_reject_message(test.xnet_messages()[0].clone()).contains("cycles are required")); +} + +#[test] +fn execute_canister_http_request_non_replicated_refund_status() { + // A non-replicated legacy request has a single participating replica, so its + // per-replica allowance equals the full refundable amount (divisor of 1). + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(10); + let mut test = ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_caller(own_subnet, caller_canister) + .build(); + + let args = CanisterHttpRequestArgs { + url: "https://example.com".to_string(), + max_response_bytes: Some(1000), + headers: BoundedHttpHeaders::new(vec![]), + body: None, + method: HttpMethod::GET, + transform: Some(TransformContext { + function: TransformFunc(candid::Func { + principal: caller_canister.get().0, + method: "transform".to_string(), + }), + context: vec![0, 1, 2], + }), + is_replicated: Some(false), + pricing_version: None, + }; + let payment = Cycles::new(1_000_000_000); + test.inject_call_to_ic00(Method::HttpRequest, args.encode(), payment); + test.execute_all(); + + let canister_http_request_contexts = &test + .state() + .metadata + .subnet_call_context_manager + .canister_http_request_contexts; + assert_eq!(canister_http_request_contexts.len(), 1); + let http_request_context = canister_http_request_contexts + .get(&CallbackId::from(0)) + .unwrap(); + assert!(matches!( + http_request_context.replication, + Replication::NonReplicated(_) + )); + + let base_fee = test.http_request_base_fee( + http_request_context.variable_parts_size(), + &http_request_context.replication, + ); + let expected_refundable = payment - base_fee.real(); + assert_eq!( + http_request_context.refund_status.refundable_cycles, + expected_refundable + ); + // A single participating replica means the allowance is the full refundable + // amount. + assert_eq!( + http_request_context.refund_status.per_replica_allowance, + expected_refundable + ); +} + +fn flexible_http_request_args(caller_canister: CanisterId) -> FlexibleCanisterHttpRequestArgs { + FlexibleCanisterHttpRequestArgs { + url: "https://example.com".to_string(), + headers: BoundedHttpHeaders::new(vec![]), + body: None, + method: HttpMethod::GET, + transform: Some(TransformContext { + function: TransformFunc(candid::Func { + principal: caller_canister.get().0, + method: "transform".to_string(), + }), + context: vec![0, 1, 2], + }), + replication: None, + } +} + +#[test] +fn execute_flexible_canister_http_request() { + for cost_schedule in [ + CanisterCyclesCostSchedule::Normal, + CanisterCyclesCostSchedule::Free, + ] { + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(10); + let mut test = ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_caller(own_subnet, caller_canister) + .with_cost_schedule(cost_schedule) + .with_flexible_http_requests_enabled() + .build(); + + let args = flexible_http_request_args(caller_canister); + let payment = Cycles::new(1_000_000_000); + test.inject_call_to_ic00(Method::FlexibleHttpRequest, args.encode(), payment); + test.execute_all(); + + let canister_http_request_contexts = &test + .state() + .metadata + .subnet_call_context_manager + .canister_http_request_contexts; + assert_eq!(canister_http_request_contexts.len(), 1); + + let http_request_context = canister_http_request_contexts + .get(&CallbackId::from(0)) + .unwrap(); + // The flexible endpoint always uses pay-as-you-go pricing and flexible + // replication. + assert_eq!( + http_request_context.pricing_version, + PricingVersion::PayAsYouGo + ); + let committee_size = match &http_request_context.replication { + Replication::Flexible { committee, .. } => committee.len(), + other => panic!("expected flexible replication, got {other:?}"), + }; + + // Pay-as-you-go takes out the entire payment upfront (unless the cost + // schedule is free), refunding everything beyond the base fee per + // replica. + let base_fee = test.http_request_base_fee( + http_request_context.variable_parts_size(), + &http_request_context.replication, + ); + let (expected_payment, expected_refundable) = match cost_schedule { + CanisterCyclesCostSchedule::Free => (payment, Cycles::new(0)), + CanisterCyclesCostSchedule::Normal => (Cycles::new(0), payment - base_fee.real()), + }; + assert_eq!(http_request_context.request.payment, expected_payment); + assert_eq!( + http_request_context.refund_status.refundable_cycles, + expected_refundable + ); + assert_eq!( + http_request_context.refund_status.per_replica_allowance, + expected_refundable / committee_size.max(1) + ); + assert_eq!( + http_request_context.refund_status.refunded_cycles, + Cycles::new(0) + ); + assert!( + http_request_context + .refund_status + .refunding_nodes + .is_empty() + ); + } +} + +#[test] +fn execute_flexible_canister_http_request_explicit_replication() { + // An explicit replication request with total_requests < subnet_size yields a + // committee smaller than the subnet, so the per-replica allowance is split + // across the committee rather than the whole subnet. + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(10); + let mut test = ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_caller(own_subnet, caller_canister) + .with_flexible_http_requests_enabled() + .build(); + + let total_requests = 4; + let mut args = flexible_http_request_args(caller_canister); + args.replication = Some(ReplicationCounts { + total_requests, + min_responses: 2, + max_responses: 4, + }); + let payment = Cycles::new(1_000_000_000); + test.inject_call_to_ic00(Method::FlexibleHttpRequest, args.encode(), payment); + test.execute_all(); + + let canister_http_request_contexts = &test + .state() + .metadata + .subnet_call_context_manager + .canister_http_request_contexts; + assert_eq!(canister_http_request_contexts.len(), 1); + let http_request_context = canister_http_request_contexts + .get(&CallbackId::from(0)) + .unwrap(); + + let (committee_size, min_responses, max_responses) = match &http_request_context.replication { + Replication::Flexible { + committee, + min_responses, + max_responses, + } => (committee.len(), *min_responses, *max_responses), + other => panic!("expected flexible replication, got {other:?}"), + }; + assert_eq!(committee_size, total_requests as usize); + assert!(committee_size < test.subnet_size()); + assert_eq!(min_responses, 2); + assert_eq!(max_responses, 4); + + // Pay-as-you-go takes the entire payment upfront and splits the refundable + // remainder across the committee. + let base_fee = test.http_request_base_fee( + http_request_context.variable_parts_size(), + &http_request_context.replication, + ); + let expected_refundable = payment - base_fee.real(); + assert_eq!(http_request_context.request.payment, Cycles::new(0)); + assert_eq!( + http_request_context.refund_status.refundable_cycles, + expected_refundable + ); + assert_eq!( + http_request_context.refund_status.per_replica_allowance, + expected_refundable / committee_size.max(1) + ); +} + +#[test] +fn execute_flexible_canister_http_request_insufficient_payment() { + // Pay-as-you-go rejects a request whose payment does not cover the base fee. + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(10); + let mut test = ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_caller(own_subnet, caller_canister) + .with_flexible_http_requests_enabled() + .build(); + + let args = flexible_http_request_args(caller_canister); + test.inject_call_to_ic00(Method::FlexibleHttpRequest, args.encode(), Cycles::new(1)); + test.execute_all(); + + // The request is rejected and no context is added. + let canister_http_request_contexts = &test + .state() + .metadata + .subnet_call_context_manager + .canister_http_request_contexts; + assert_eq!(canister_http_request_contexts.len(), 0); +} + +#[test] +fn execute_flexible_canister_http_request_disabled() { + // The flexible HTTP outcalls feature is gated behind a feature flag that is + // disabled by default. + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(10); + let mut test = ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_caller(own_subnet, caller_canister) + .build(); + + let args = flexible_http_request_args(caller_canister); + test.inject_call_to_ic00( + Method::FlexibleHttpRequest, + args.encode(), + Cycles::new(1_000_000_000), + ); + test.execute_all(); + + // No context is added and the request is rejected specifically because the + // feature flag is disabled (as opposed to any other rejection reason). + let canister_http_request_contexts = &test + .state() + .metadata + .subnet_call_context_manager + .canister_http_request_contexts; + assert_eq!(canister_http_request_contexts.len(), 0); + assert_eq!( + get_reject_message(test.xnet_messages()[0].clone()), + "This API is not enabled on this subnet" + ); +} + fn get_reject_message(response: RequestOrResponse) -> String { match response { RequestOrResponse::Request(_) => panic!("Expected Response"), diff --git a/rs/test_utilities/execution_environment/src/lib.rs b/rs/test_utilities/execution_environment/src/lib.rs index 4dc8435a14ab..f131cccab4cd 100644 --- a/rs/test_utilities/execution_environment/src/lib.rs +++ b/rs/test_utilities/execution_environment/src/lib.rs @@ -74,6 +74,7 @@ use ic_types::messages::{Blob, RawSignedSenderInfo, SignedIngressContent, Signed use ic_types::{ CanisterId, Height, NumInstructions, QueryStatsEpoch, Time, UserId, batch::QueryStats, + canister_http::Replication, crypto::{AlgorithmId, canister_threshold_sig::MasterPublicKey}, ingress::{IngressState, IngressStatus, WasmResult}, messages::{ @@ -570,6 +571,18 @@ impl ExecutionTest { ) } + pub fn http_request_base_fee( + &self, + request_size: NumBytes, + replication: &Replication, + ) -> CompoundCycles { + self.cycles_account_manager.http_request_base_fee( + request_size, + replication, + self.get_own_subnet_cycles_config(), + ) + } + pub fn reduced_wasm_compilation_fee(&self, wasm: &[u8]) -> Cycles { let cost = wasm_compilation_cost(wasm); self.convert_instructions_to_cycles( @@ -2656,6 +2669,11 @@ impl ExecutionTestBuilder { self } + pub fn with_flexible_http_requests_enabled(mut self) -> Self { + self.execution_config.flexible_http_requests = FlagStatus::Enabled; + self + } + pub fn without_composite_queries(mut self) -> Self { self.execution_config.composite_queries = FlagStatus::Disabled; self diff --git a/rs/tests/networking/canister_http_flexible_test.rs b/rs/tests/networking/canister_http_flexible_test.rs index a176fcde7a45..9848c23dbe3e 100644 --- a/rs/tests/networking/canister_http_flexible_test.rs +++ b/rs/tests/networking/canister_http_flexible_test.rs @@ -103,7 +103,7 @@ async fn test_proxy_canister(proxy_canister: &Canister<'_>, url: String, logger: .await .expect("Update call to proxy canister failed"); - let expected_error_msg = "FlexibleHttpRequest is not yet implemented"; + let expected_error_msg = "This API is not enabled on this subnet"; match res { Ok(_) => { diff --git a/rs/types/types/src/canister_http.rs b/rs/types/types/src/canister_http.rs index 8a3377305bff..7a09536d538c 100644 --- a/rs/types/types/src/canister_http.rs +++ b/rs/types/types/src/canister_http.rs @@ -144,10 +144,10 @@ pub struct CanisterHttpRequestContext { #[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)] pub struct RefundStatus { /// The amount of cycles that are available to be refunded for this request. - /// The amount is calculated based to the payment of the request. + /// The amount is calculated based on the payment of the request. pub refundable_cycles: Cycles, /// The amount of cycles that are allowed to be refunded for this request. - /// The allowance is calculated based on the subnet size: per_replica_allowance = refundable_cycles / subnet_size. + /// The allowance is calculated based on the committee size: per_replica_allowance = refundable_cycles / committee_size. pub per_replica_allowance: Cycles, /// The amount of cycles that have already been refunded for this request. /// Invariant: refunded_cycles <= refundable_cycles @@ -585,13 +585,9 @@ impl CanisterHttpRequestContext { .unwrap_or(DEFAULT_HTTP_OUTCALLS_PRICING_VERSION); PricingVersion::from_repr(final_version_u32).unwrap_or(PricingVersion::Legacy) }, - refund_status: RefundStatus { - //TODO(IC-1937): subtract the base fee from the refundable amount. - refundable_cycles: request.payment, - per_replica_allowance: request.payment / node_ids.len(), - refunded_cycles: Cycles::new(0), - refunding_nodes: BTreeSet::new(), - }, + // The refund status is populated in `try_add_http_context_to_replicated_state` + // based on the request's payment and the base fee. + refund_status: RefundStatus::default(), // TODO: populate with the actual registry version this request is processed at. registry_version: RegistryVersion::from(0), }) @@ -694,13 +690,9 @@ impl CanisterHttpRequestContext { max_responses, }, pricing_version: PricingVersion::PayAsYouGo, - refund_status: RefundStatus { - //TODO(IC-1937): subtract the base fee from the refundable amount. - refundable_cycles: request.payment, - per_replica_allowance: request.payment / (total_requests as usize).max(1), - refunded_cycles: Cycles::new(0), - refunding_nodes: BTreeSet::new(), - }, + // The refund status is populated in `try_add_http_context_to_replicated_state` + // based on the request's payment and the base fee. + refund_status: RefundStatus::default(), // TODO: populate with the actual registry version this request is processed at. registry_version: RegistryVersion::from(0), }) From 696e58b689d4e301a5e8d050df6ca66b57dfd019 Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic <73236646+NikolaMilosa@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:01:20 +0200 Subject: [PATCH 69/75] feat: allow unlimited number of gen4 nodes (#10448) This pull request allows node providers to add more type4.x nodes (excluding type4.5 for now see #10393). This is needed to allow for provisioning on-demand nodes in the cloud and registering them with the network. --------- Co-authored-by: IDX GitHub Automation --- .../mutations/node_management/do_add_node.rs | 115 ++++++++++++++++-- rs/registry/canister/unreleased_changelog.md | 14 +++ 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/rs/registry/canister/src/mutations/node_management/do_add_node.rs b/rs/registry/canister/src/mutations/node_management/do_add_node.rs index f5bc0c26a2df..0339186907c2 100644 --- a/rs/registry/canister/src/mutations/node_management/do_add_node.rs +++ b/rs/registry/canister/src/mutations/node_management/do_add_node.rs @@ -37,6 +37,19 @@ use ic_registry_keys::{NODE_REWARDS_TABLE_KEY, make_replica_version_key}; use ic_types::{crypto::CurrentNodePublicKeys, time::Time}; use prost::Message; +/// Effective per-node-operator cap on the number of `type4.1`-`type4.4` nodes +/// that may be registered, used in lieu of the standard +/// `max_rewardable_nodes`-based quota for those reward types. +/// +/// TODO(CLO-15): Remove this constant and the associated special case in +/// `do_add_node_` once the reward canister no longer treats `type4.5` rewards +/// as `type1.1`. Until then, we cannot meaningfully size +/// `max_rewardable_nodes` for the `type4.x` family without blocking legitimate +/// gen4 onboarding, so we substitute a large-but-bounded sentinel here. The +/// value is chosen to be comfortably above any realistic per-operator +/// deployment while still preventing runaway registrations. +const EXCESSIVE_NUMBER_OF_TYPE_4_NODES: u32 = 1_000; + impl Registry { /// Adds a new node to the registry. /// @@ -136,10 +149,26 @@ impl Registry { "{LOG_PREFIX}do_add_node: Node reward type is required." ))?; - let max_rewardable_nodes_same_type = *node_operator_record - .max_rewardable_nodes - .get(&(node_reward_type.to_string())) - .ok_or(format!("{LOG_PREFIX}do_add_node: Node Operator does not have rewardable nodes for {node_reward_type}"))?; + // See `EXCESSIVE_NUMBER_OF_TYPE_4_NODES` at the top of this file for + // why type4.1-type4.4 are handled via a sentinel quota rather than + // the per-operator `max_rewardable_nodes` map. Type4.5 is + // explicitly excluded from this exemption. + let is_permissionless_type_4_x = matches!( + node_reward_type, + NodeRewardType::Type4dot1 + | NodeRewardType::Type4dot2 + | NodeRewardType::Type4dot3 + | NodeRewardType::Type4dot4 + ); + + let max_rewardable_nodes_same_type = if is_permissionless_type_4_x { + EXCESSIVE_NUMBER_OF_TYPE_4_NODES + } else { + *node_operator_record + .max_rewardable_nodes + .get(&(node_reward_type.to_string())) + .ok_or(format!("{LOG_PREFIX}do_add_node: Node Operator does not have rewardable nodes for {node_reward_type}"))? + }; let num_in_registry_same_type = get_node_operator_nodes(self, caller_id) .into_iter() @@ -147,10 +176,14 @@ impl Registry { .filter(|t| t == &(node_reward_type as i32)) .count() as u32; - // Validate node operator's max_rewardable_nodes quota - if max_rewardable_nodes_same_type - <= num_in_registry_same_type.saturating_sub(num_removed_same_ip_same_type) - { + // Validate node operator's max_rewardable_nodes quota. + // TODO(@pietrodimarco-dfinity): confirm whether `>=` is intended + // here (i.e. `num_remaining_nodes == max_rewardable_nodes_same_type` + // should reject) or whether this should be `>`. Preserving the + // pre-existing behavior for now. + let num_remaining_nodes = + num_in_registry_same_type.saturating_sub(num_removed_same_ip_same_type); + if num_remaining_nodes >= max_rewardable_nodes_same_type { return Err(format!( "{LOG_PREFIX}do_add_node: Node Operator has reached max_rewardable_nodes quota for {node_reward_type}.\ Number of nodes in the registry with {node_reward_type} type = {num_in_registry_same_type},\ @@ -1124,6 +1157,72 @@ mod tests { .unwrap(); } + #[test] + fn should_allow_arbitrarily_many_nodes_of_type4dot1_through_type4dot4() { + // TODO(CLO-15): Remove this test once we re-enable the + // max_rewardable_nodes quota check for type4.1-type4.4. See related TODO + // in `do_add_node_`. + for node_reward_type in [ + NodeRewardType::Type4dot1, + NodeRewardType::Type4dot2, + NodeRewardType::Type4dot3, + NodeRewardType::Type4dot4, + ] { + let mut registry = invariant_compliant_registry(0); + + let (mutate_request, node_ids_and_dkg_pks) = prepare_registry_with_nodes(1, 1); + registry.maybe_apply_mutation_internal(mutate_request.mutations); + let node_ids: Vec = node_ids_and_dkg_pks.keys().cloned().collect(); + // Note: `max_rewardable_nodes` intentionally does NOT contain + // an entry for `node_reward_type` here, demonstrating that the + // quota does not apply for these types. + let node_operator_id = + registry_add_node_operator_for_node(&mut registry, node_ids[0], btreemap! {}); + + // Adding several nodes of this type should all succeed even + // though no quota is configured. Note that for other reward types + // this would fail; the immediately following + // `should_panic_if_max_rewardable_nodes_is_exhausted_for_type4dot5` + // test demonstrates that the underlying quota check is still + // exercised, so passing here is meaningful and not simply due to + // the check being a no-op. + for i in 0..3_u8 { + let (payload, _) = prepare_add_node_payload(10 + i, node_reward_type); + registry + .do_add_node_(payload, node_operator_id, now_system_time()) + .unwrap_or_else(|e| { + panic!("do_add_node_ failed for {node_reward_type} on iteration {i}: {e}") + }); + } + } + } + + #[test] + #[should_panic( + expected = "[Registry] do_add_node: Node Operator has reached max_rewardable_nodes quota for type4.5" + )] + fn should_panic_if_max_rewardable_nodes_is_exhausted_for_type4dot5() { + // type4.5 is explicitly excluded from the type4.1-type4.4 exemption + // because rewards of type4.5 are currently treated as type1.1. The + // standard max_rewardable_nodes quota check therefore still applies. + let mut registry = invariant_compliant_registry(0); + + let (mutate_request, node_ids_and_dkg_pks) = prepare_registry_with_nodes(1, 1); + registry.maybe_apply_mutation_internal(mutate_request.mutations); + let node_ids: Vec = node_ids_and_dkg_pks.keys().cloned().collect(); + let node_operator_id = registry_add_node_operator_for_node( + &mut registry, + node_ids[0], + btreemap! { NodeRewardType::Type4dot5 => 0 }, + ); + + let (payload, _valid_pks) = prepare_add_node_payload(2, NodeRewardType::Type4dot5); + + registry + .do_add_node_(payload, node_operator_id, now_system_time()) + .unwrap(); + } + #[test] fn test_node_reward_type_is_required() { let mut registry = invariant_compliant_registry(0); diff --git a/rs/registry/canister/unreleased_changelog.md b/rs/registry/canister/unreleased_changelog.md index b74d0b6fdf37..90bea93b1692 100644 --- a/rs/registry/canister/unreleased_changelog.md +++ b/rs/registry/canister/unreleased_changelog.md @@ -24,6 +24,20 @@ on the process that this file is part of, see ## Changed +* Temporarily bypass the per-operator `max_rewardable_nodes` quota check in + `add_node` for node reward types `type4.1` through `type4.4`. Instead of the + configured quota, these types are subjected to a single high sentinel cap + (`EXCESSIVE_NUMBER_OF_TYPE_4_NODES`, currently 1000 per node operator), + chosen to be well above any realistic per-operator deployment while still + preventing runaway registrations. `type4.5` is explicitly excluded and + remains subject to the standard `max_rewardable_nodes` quota. + + Motivation: node providers are starting to deploy gen4 hardware now, but the + reward canister currently still treats `type4.5` rewards as `type1.1`, which + means we cannot yet meaningfully size `max_rewardable_nodes` quotas for the + `type4.x` family. Enforcing the quota in the meantime would block legitimate + gen4 onboarding. The quota check will be restored once the reward-side + handling of `type4.5` is fixed (see CLO-15). * One-time post-upgrade migration converting the reward type of 100 currently unassigned nodes from `type1.1` to `type4.5`. The migration only mutates nodes whose reward type is still `type1.1`, so it is idempotent across upgrades. From 5dfdb9e98b90cd7b3de2c9369c79ca7dfaac7424 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Fri, 19 Jun 2026 16:24:13 +0200 Subject: [PATCH 70/75] fix: CONTAINER_RUNTIME=docker ./ci/container/container-run.sh (#10521) This is to fix: ``` $ CONTAINER_RUNTIME=docker ./ci/container/container-run.sh ... jq: error (at :1): Cannot index string with string "Type" jq: error (at :2): Cannot index string with string "Type" jq: error (at :3): Cannot index string with string "Type" jq: error (at :4): Cannot index string with string "Type" ``` --- ci/container/container-run.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ci/container/container-run.sh b/ci/container/container-run.sh index 7d36a285a857..1e54ae3a975f 100755 --- a/ci/container/container-run.sh +++ b/ci/container/container-run.sh @@ -125,11 +125,14 @@ if ! "${image_exists_cmd[@]}" >/dev/null 2>&1; then fi fi -if [ "$DEVENV" = true ]; then - # on the devenv we issue a warning if the images start taking up a lot of space. - # Podman does not have a dedicated layer cache like docker, so we avoid nuking dangling/unused images unless space becomes a concern; - # this allows new image builds to benefit from cached layers. - # We only issue a warning so that the user can GC when it's most convenient. +# On the devenv we issue a warning if the images start taking up a lot of space. +# Podman does not have a dedicated layer cache like docker, so we avoid nuking dangling/unused images unless space becomes a concern; +# this allows new image builds to benefit from cached layers. +# We only issue a warning so that the user can GC when it's most convenient. +# This is podman-specific: docker manages its own layer cache and reports image +# sizes in a different JSON shape (a stream of objects without a RawSize field), +# so we skip the check under docker. +if [ "$DEVENV" = true ] && [ "$RUNTIME" = podman ]; then MAX_GB=20 images_rawsize=$("${CONTAINER_CMD[@]}" system df --format json | jq -cMr '.[]|select(.Type == "Images")|.RawSize') if ((images_rawsize > MAX_GB * 10 ** 9)); then From 1a7aa6cd4ea24fc65b505bc5cd17353b8a98f639 Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Sat, 20 Jun 2026 07:38:54 +0000 Subject: [PATCH 71/75] refactor: Allow references on the RHS of left_outer_join() For cases when cloning/copying the key type is expensive, it should ideally be possible to have the RHS iterate by reference. --- rs/utils/src/iter.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/rs/utils/src/iter.rs b/rs/utils/src/iter.rs index 47eca804a509..ff135c0afc5e 100644 --- a/rs/utils/src/iter.rs +++ b/rs/utils/src/iter.rs @@ -1,5 +1,6 @@ //! Iterator helpers. +use std::borrow::Borrow; use std::cmp::Ordering; /// Performs a left outer join of two key-value iterators. @@ -26,12 +27,16 @@ use std::cmp::Ordering; /// ] /// ); /// ``` -pub fn left_outer_join<'l, 'r, L, R, K, LV, RV>(left: L, right: R) -> LeftOuterJoin +pub fn left_outer_join<'l, 'r, L, R, K, LV, RK, RV>( + left: L, + right: R, +) -> LeftOuterJoin where L: Iterator, - R: Iterator, + R: Iterator, K: Ord + 'l + 'r, LV: 'l, + RK: Borrow, RV: 'r, { let mut right_iter = right; @@ -44,22 +49,24 @@ where } /// Iterator produced by `left_outer_join`. -pub struct LeftOuterJoin +pub struct LeftOuterJoin where L: Iterator, - R: Iterator, + R: Iterator, K: Ord, + RK: Borrow, { left: L, right: R, - right_peek: Option<(K, RV)>, + right_peek: Option<(RK, RV)>, } -impl Iterator for LeftOuterJoin +impl Iterator for LeftOuterJoin where L: Iterator, - R: Iterator, + R: Iterator, K: Ord, + RK: Borrow, { type Item = (K, LV, Option); @@ -70,7 +77,7 @@ where let right_cmp = self .right_peek .as_ref() - .map(|(right_key, _)| right_key.cmp(&left_key)); + .map(|(right_key, _)| right_key.borrow().cmp(&left_key)); match right_cmp { None => { From 38688d4ca9fd4bbbce72b077a1db23b686295349 Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Sat, 20 Jun 2026 07:45:15 +0000 Subject: [PATCH 72/75] chore: Clean up comments in hash_tree.rs This is a minor clean-up change aimed at keeping the HashTree stubbing change a bit more focused. No code changes, only comments. --- rs/canonical_state/tree_hash/src/hash_tree.rs | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/rs/canonical_state/tree_hash/src/hash_tree.rs b/rs/canonical_state/tree_hash/src/hash_tree.rs index 888812eb38c3..edf349be8afe 100644 --- a/rs/canonical_state/tree_hash/src/hash_tree.rs +++ b/rs/canonical_state/tree_hash/src/hash_tree.rs @@ -45,13 +45,13 @@ enum NodeKind { Node, } -/// NodeId describe the position of a node in the HashTree data structure +/// The position of a node in the HashTree data structure. /// /// HashTree consists of several parallel vectors of vectors. The kind of node /// is node_id.kind(), the first index is node_id.bucket(), whereas the second /// index is node_id.index(). /// -/// For example, the digest of a node_id with node_id.kind() = NodeKind::Fork can +/// For example, the digest of a node_id with node_id.kind() = NodeKind::Fork /// is stored at hash_tree.fork_digests[node_id.bucket()][node_id.index()] /// /// The reason for storing vectors of vectors is because it lends itself to parallelism @@ -74,7 +74,7 @@ impl fmt::Debug for NodeId { } impl NodeId { - /// Constructs a node id for an empty tree. + /// Constructs a node ID for an empty tree. #[inline] fn empty() -> Self { Self { @@ -83,7 +83,7 @@ impl NodeId { } } - /// Constructs a node id for a new Fork with the specified index. + /// Constructs a node ID for a new `Fork` with the specified index. #[inline] fn fork(bucket: usize, idx: usize) -> Result { if idx > INDEX_MASK as usize { @@ -98,7 +98,7 @@ impl NodeId { } } - /// Constructs a node id for a new Leaf with the specified index. + /// Constructs a node ID for a new `Leaf` with the specified index. #[inline] fn leaf(bucket: usize, idx: usize) -> Result { if idx > INDEX_MASK as usize { @@ -113,7 +113,7 @@ impl NodeId { } } - /// Constructs a node id for a new Node with the specified index. + /// Constructs a node ID for a new `Node` with the specified index. #[inline] fn node(bucket: usize, idx: usize) -> Result { if idx > INDEX_MASK as usize { @@ -153,8 +153,8 @@ impl NodeId { } } -/// A range of NodeIds that share the same bucket and have consecutive indices -/// index_range is to be understood as a half-open range, i.e., `start <= x < end`. +/// A range of `NodeIds` that share the same bucket and have consecutive indices. +/// `index_range` is a half-open range, i.e., `start <= x < end`. #[derive(Clone, Debug, Default)] struct NodeIndexRange { bucket: usize, @@ -242,7 +242,7 @@ pub struct HashTree { /// index_range.0 <= index_range.1 <= node_labels[bucket].len() root_labels_range: NodeIndexRange, - /// (i,j)-th element of this array contains the hash of the leaf with id + /// (i,j)-th element of this array contains the hash of the leaf with ID /// `NodeId::leaf(i,j)`. leaf_digests: Vec>, @@ -250,14 +250,14 @@ pub struct HashTree { // fork_digests.len() == fork_left_children.len() == fork_right_children.len(). // forall i: fork_digest[i].len() == fork_left_children[i].len() == // fork_right_children[i].len() - /// (i,j)-th element of this array contains the hash of the fork with id equal + /// (i,j)-th element of this array contains the hash of the fork with ID equal /// to `NodeId::fork(i,j)`. fork_digests: Vec>, - /// (i,j)-th element of this array contains the node id of the left child of the - /// fork with id `NodeId::fork(i,j)`. + /// (i,j)-th element of this array contains the node ID of the left child of the + /// fork with ID `NodeId::fork(i,j)`. fork_left_children: Vec>, - /// (i,j)-th element of this array contains the node id of the right child of - /// the fork with id `NodeId::fork(i,j)`. + /// (i,j)-th element of this array contains the node ID of the right child of + /// the fork with ID `NodeId::fork(i,j)`. fork_right_children: Vec>, // INVARIANT: @@ -268,19 +268,18 @@ pub struct HashTree { // // INVARIANT: // labels having the same parent node are stored consecutively in the same bucket. - /// (i,j)-th element of this array contains the hash of the labeled node with id + /// (i,j)-th element of this array contains the hash of the labeled node with ID /// `NodeId::node(i,j)`. node_digests: Vec>, /// (i,j)-th element of this array contains the label of the labeled node with - /// id `NodeId::node(i,j)`. + /// ID `NodeId::node(i,j)`. node_labels: Vec>, /// (i,j)-th element of this array contains the direct child of the labeled node - /// with id `NodeId::node(i,j)`. + /// with ID `NodeId::node(i,j)`. node_children: Vec>, - /// (i,j)-th element of this array contains an IndexRange pointing to a - /// half-closed index interval [a, b) in of of the buckets - /// pointing into the node_labels array containing all the labels on edges - /// of the original tree going out of the node with id `NodeId::node(i,j)`. + /// (i,j)-th element of this array points to a bucket and a half-open index + /// interval `[a, b)` in the `node_labels` array, covering all labels on edges + /// of the original tree going out of the node with ID `NodeId::node(i,j)`. /// /// INVARIANT: bucket ≤ node_labels.len() /// index_range.0 <= index_range.1 <= node_labels[bucket].len() @@ -310,7 +309,7 @@ impl HashTree { } } - /// Number of digests in the HashTree + /// Number of digests in the `HashTree`. pub fn size(&self) -> usize { let leaf_size: usize = self.leaf_digests.iter().map(|bucket| bucket.len()).sum(); let fork_size: usize = self.fork_digests.iter().map(|bucket| bucket.len()).sum(); @@ -320,7 +319,7 @@ impl HashTree { leaf_size + fork_size + node_size } - /// Largest index in the HashTree + /// Largest index in the `HashTree`. pub fn max_index(&self) -> usize { let leaf_size = self .leaf_digests From a82b7778082a4e75ac74269b1bdf0d257eeb03b4 Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Mon, 22 Jun 2026 07:36:49 +0000 Subject: [PATCH 73/75] feat: [MR-144] Reuse unchanged canister subtree digests across `HashTrees` `hash_lazy_tree` re-hashes every canister's subtree each round, even though almost all canisters are untouched. This PR lets a new `HashTree` reuse subtree digests from a previous one, keyed by the canister's backing `Arc` (shared copy-on-write, replaced only on mutation). - **Stub nodes.** A new `NodeKind::Stub` collapses a reusable subtree to a digest-only node. A `LazyFork` opts in via `subtree_source()`; `CanisterFork` returns a `SubtreeSource` over its `Arc`. Other forks are still materialized inline. - **`SubtreeSource`.** A type-erased, owned handle (`Arc` + expander fn pointer) that keeps the source alive (no ABA on the address) and re-expands the stub for witnesses. `Eq` is a conservative, false-negative-only reuse gate: same source allocation **and** same expander. - **Version in the expander.** `expand_canister::` bakes the certification version into the function pointer, so it isn't stored per stub and reuse can't mix versions. - **`hash_lazy_tree_with_baseline`.** Lockstep-joins the new tree against a baseline; matching `SubtreeSource`s reuse the baseline digest, otherwise rebuild. Witnesses expand stubs via `SubtreeSource::expand()`, so the tree stays self-contained. - **Adaptive parallelization.** Forks build sequentially, then hand the *remaining* children to the thread pool once the observed rebuild rate (after a warmup) projects more than `PARALLEL_MIN_CHILDREN` rebuilds. The common reuse-heavy path stays sequential; large/from-scratch forks parallelize. New `tests/subtree.rs`: stubbing, witness/absence-proof expansion from source, baseline builds (full and partial change) matching from-scratch, and `reuse_is_by_identity_not_by_value` (shared `Arc`s reuse, replaced `Arc`s rebuild). Actually invoke `hash_lazy_tree_with_baseline` from production code, with the latest available `HashTree`. For now, it is only invoked from the benchmark and tests. Stubbing is wired up for `/canisters` only; ingress `/messages` are left for later. --- Cargo.lock | 1 + .../src/lazy_tree_conversion.rs | 55 +- rs/canonical_state/tree_hash/BUILD.bazel | 1 + rs/canonical_state/tree_hash/Cargo.toml | 1 + rs/canonical_state/tree_hash/src/hash_tree.rs | 479 +++++++++++++++--- rs/canonical_state/tree_hash/src/lazy_tree.rs | 109 ++++ rs/canonical_state/tree_hash/tests/subtree.rs | 433 ++++++++++++++++ rs/state_manager/benches/bench_traversal.rs | 43 +- 8 files changed, 1043 insertions(+), 79 deletions(-) create mode 100644 rs/canonical_state/tree_hash/tests/subtree.rs diff --git a/Cargo.lock b/Cargo.lock index c2e1fc00702c..f90f2434bb91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7456,6 +7456,7 @@ dependencies = [ "ic-crypto-test-utils-reproducible-rng", "ic-crypto-tree-hash", "ic-crypto-tree-hash-test-utils", + "ic-utils 0.9.0", "itertools 0.12.1", "leb128", "proptest", diff --git a/rs/canonical_state/src/lazy_tree_conversion.rs b/rs/canonical_state/src/lazy_tree_conversion.rs index aa85cf097c3d..3e8a6d7d7181 100644 --- a/rs/canonical_state/src/lazy_tree_conversion.rs +++ b/rs/canonical_state/src/lazy_tree_conversion.rs @@ -9,9 +9,10 @@ use crate::{ }; use LazyTree::Blob; use ic_canonical_state_tree_hash::{ - hash_tree::HashTree, + hash_tree::{HashTree, HashTreeError, hash_lazy_tree}, lazy_tree::{ - Lazy, LazyFork, LazyTree, blob, fork, materialize::materialize_partial, num, string, + Lazy, LazyFork, LazyTree, SubtreeExpander, SubtreeSource, blob, fork, + materialize::materialize_partial, num, string, }, }; use ic_crypto_tree_hash::{Label, Witness, sparse_labeled_tree_from_paths}; @@ -786,7 +787,7 @@ const CANISTER_NO_MODULE_LABELS: [&[u8]; 1] = [CONTROLLERS_LABEL]; #[derive(Clone)] struct CanisterFork<'a> { - canister: &'a CanisterState, + canister: &'a Arc, version: CertificationVersion, } @@ -849,6 +850,54 @@ impl<'a> LazyFork<'a> for CanisterFork<'a> { fn len(&self) -> usize { self.valid_labels().len() } + + /// A canister's certified subtree is stored as a reusable stub identified by + /// the backing `Arc` and the version-specific expander. An + /// unchanged canister keeps the same `Arc` (copy-on-write) and the same + /// expander, so its precomputed digest is reused from the baseline; any + /// mutation or version change yields a mismatched [`SubtreeSource`] and a + /// rebuild. + fn subtree_source(&self) -> Option { + Some(SubtreeSource::new( + self.canister, + select_canister_expander(self.version), + )) + } +} + +/// Rebuilds a canister's stubbed [subtree](`NodeKind::Stub`) for witness +/// generation, by recovering the `Arc` from the stub's +/// [`SubtreeSource`] and traversing its [`CanisterFork`]. +/// +/// The certification version (which the canonical encoding depends on) is baked +/// in as the const parameter `V`, so the stored function pointer alone fully +/// determines the expansion — see [`select_canister_expander`]. +fn expand_canister(source: &SubtreeSource) -> Result { + let canister = source.downcast::(); + let version = CertificationVersion::try_from(V) + .expect("const version parameter is a valid certification version"); + // `canister` (and thus the borrow below) outlives `hash_lazy_tree`, which + // returns an owned `HashTree`; no borrow escapes. + hash_lazy_tree(&fork(CanisterFork { + canister: &canister, + version, + })) +} + +/// Selects the [`expand_canister`] monomorphization for `version`, so the +/// resulting [`SubtreeExpander`] function pointer carries the version with it +/// (rather than replicating it in every stub). +fn select_canister_expander(version: CertificationVersion) -> SubtreeExpander { + match version { + CertificationVersion::V19 => expand_canister::<{ CertificationVersion::V19 as u32 }>, + CertificationVersion::V20 => expand_canister::<{ CertificationVersion::V20 as u32 }>, + CertificationVersion::V21 => expand_canister::<{ CertificationVersion::V21 as u32 }>, + CertificationVersion::V22 => expand_canister::<{ CertificationVersion::V22 as u32 }>, + CertificationVersion::V23 => expand_canister::<{ CertificationVersion::V23 as u32 }>, + CertificationVersion::V24 => expand_canister::<{ CertificationVersion::V24 as u32 }>, + CertificationVersion::V25 => expand_canister::<{ CertificationVersion::V25 as u32 }>, + CertificationVersion::V26 => expand_canister::<{ CertificationVersion::V26 as u32 }>, + } } fn api_boundary_nodes_as_tree( diff --git a/rs/canonical_state/tree_hash/BUILD.bazel b/rs/canonical_state/tree_hash/BUILD.bazel index 058109dcb4b2..ff93981289d7 100644 --- a/rs/canonical_state/tree_hash/BUILD.bazel +++ b/rs/canonical_state/tree_hash/BUILD.bazel @@ -10,6 +10,7 @@ rust_library( deps = [ # Keep sorted. "//rs/crypto/tree_hash", + "//rs/utils", "@crate_index//:itertools", "@crate_index//:leb128", "@crate_index//:scoped_threadpool", diff --git a/rs/canonical_state/tree_hash/Cargo.toml b/rs/canonical_state/tree_hash/Cargo.toml index c3793bff62ad..1594549c40ba 100644 --- a/rs/canonical_state/tree_hash/Cargo.toml +++ b/rs/canonical_state/tree_hash/Cargo.toml @@ -8,6 +8,7 @@ documentation.workspace = true [dependencies] ic-crypto-tree-hash = { path = "../../crypto/tree_hash" } +ic-utils = { path = "../../utils" } itertools = { workspace = true } leb128 = "0.2.1" scoped_threadpool = "0.1.*" diff --git a/rs/canonical_state/tree_hash/src/hash_tree.rs b/rs/canonical_state/tree_hash/src/hash_tree.rs index edf349be8afe..de0ab9ab7fa9 100644 --- a/rs/canonical_state/tree_hash/src/hash_tree.rs +++ b/rs/canonical_state/tree_hash/src/hash_tree.rs @@ -1,8 +1,9 @@ -use crate::lazy_tree::{LazyFork, LazyTree}; +use crate::lazy_tree::{LazyTree, SubtreeSource}; use crypto::WitnessGenerationError; use ic_crypto_tree_hash::{ self as crypto, Digest, Label, LabeledTree, WitnessBuilder, hasher::Hasher, }; +use ic_utils::iter::left_outer_join; use itertools::izip; use std::fmt; use std::iter::repeat_with; @@ -16,6 +17,17 @@ const NUMBER_OF_CERTIFICATION_THREADS: u32 = 16; /// the depth of the lazy tree. const MAX_RECURSION_DEPTH: u32 = 128; +/// A fork with fewer than this many (expensive to build) children is always +/// built sequentially: it is too small for the thread pool to pay for itself. +pub const PARALLEL_MIN_CHILDREN: usize = 1000; + +/// When building against a baseline, a large fork starts building sequentially +/// and samples this many children before extrapolating, over the whole fork, the +/// rate at which they have to be *built* — i.e. hashed, rather than cheaply +/// reused from the baseline. With no baseline nothing can be reused, so the +/// switch to the thread pool happens immediately, without a warmup. +const ADAPTIVE_WARMUP_CHILDREN: usize = 1000; + /// SHA256 of the domain separator "ic-hashtree-empty" const EMPTY_HASH: Digest = Digest([ 0x4e, 0x3e, 0xd3, 0x5c, 0x4e, 0x2d, 0x1e, 0xe8, 0x99, 0x96, 0x48, 0x3f, 0xb6, 0x26, 0x0a, 0x64, @@ -29,13 +41,14 @@ const EMPTY_LEAF_HASH: Digest = Digest([ 0xc0, 0x2a, 0x23, 0xa5, 0x1e, 0x08, 0x98, 0xbc, 0x2c, 0x4e, 0x32, 0x3f, 0xce, 0x0e, 0x62, 0x2c, ]); -/// 30 LSBs are used to store the index -const INDEX_MASK: u32 = 0x3fff_ffff; -/// 2 MSBs are used to store the node kind -const KIND_MASK: u32 = 0xc000_0000; -const LEAF_KIND: u32 = 0x4000_0000; -const NODE_KIND: u32 = 0x8000_0000; -const FORK_KIND: u32 = 0xc000_0000; +/// 29 LSBs are used to store the index +const INDEX_MASK: u32 = 0x1fff_ffff; +/// 3 MSBs are used to store the node kind +const KIND_MASK: u32 = 0xe000_0000; +const LEAF_KIND: u32 = 0x2000_0000; +const NODE_KIND: u32 = 0x4000_0000; +const FORK_KIND: u32 = 0x6000_0000; +const STUB_KIND: u32 = 0x8000_0000; #[derive(Copy, Clone, Eq, PartialEq, Debug)] enum NodeKind { @@ -43,6 +56,12 @@ enum NodeKind { Fork, Leaf, Node, + /// A subtree reduced to a single root digest plus the [`SubtreeSource`] (source + /// `Arc`) that it was built from. When the actual subtree is needed for a + /// witness, it is materialized on demand from the source. When an unchanged + /// subtree (equal `SubtreeSource` and certification version) is found in a + /// baseline tree, its digest is reused instead of being recomputed. + Stub, } /// The position of a node in the HashTree data structure. @@ -69,6 +88,7 @@ impl fmt::Debug for NodeId { NodeKind::Fork => write!(f, "Fork({}, {})", self.bucket(), self.index()), NodeKind::Leaf => write!(f, "Leaf({}, {})", self.bucket(), self.index()), NodeKind::Node => write!(f, "Node({}, {})", self.bucket(), self.index()), + NodeKind::Stub => write!(f, "Stub({}, {})", self.bucket(), self.index()), } } } @@ -128,6 +148,21 @@ impl NodeId { } } + /// Constructs a node ID for a new `Stub` with the specified index. + #[inline] + fn stub(bucket: usize, idx: usize) -> Result { + if idx > INDEX_MASK as usize { + Err(HashTreeError::IndexOverflow) + } else { + Ok(Self { + bucket: bucket + .try_into() + .map_err(|_| HashTreeError::IndexOverflow)?, + index_and_kind: STUB_KIND | idx as u32, + }) + } + } + /// Returns the component kind of this node. #[inline] fn kind(self) -> NodeKind { @@ -136,6 +171,7 @@ impl NodeId { FORK_KIND => NodeKind::Fork, NODE_KIND => NodeKind::Node, LEAF_KIND => NodeKind::Leaf, + STUB_KIND => NodeKind::Stub, _ => NodeKind::Empty, } } @@ -210,17 +246,18 @@ impl NodeIndexRange { /// /// In this representation, the identifier of a node are two 32 bit unsigned /// integers, where the first number indexes into the (outer) vector and for -/// the second number , the 2 most significant bits are used to indicate the +/// the second number , the 3 most significant bits are used to indicate the /// type of the node: /// -/// * (0,00) is an empty tree. -/// * (0,01) is a leaf. -/// * (0,10) is a labeled node. -/// * (0,11) is a fork. +/// * (0,000) is an empty tree. +/// * (0,001) is a leaf. +/// * (0,010) is a labeled node. +/// * (0,011) is a fork. +/// * (0,100) is a reusable stub. /// -/// This means that the tree can store at most 2^30 nodes of the same type. As +/// This means that the tree can store at most 2^29 nodes of the same type. As /// each tree node has a 32-byte hash associated with it, the tree needs to -/// occupy at least 32 GiB of data before the index overflows. +/// occupy at least 16 GiB of data before the index overflows. /// /// [1]: https://en.wikipedia.org/wiki/AoS_and_SoA #[derive(Clone, Debug)] @@ -284,6 +321,32 @@ pub struct HashTree { /// INVARIANT: bucket ≤ node_labels.len() /// index_range.0 <= index_range.1 <= node_labels[bucket].len() node_children_labels_ranges: Vec>, + + /// (i,j)-th element of this array contains the stub with ID `NodeId::stub(i,j)`: + /// the subtree's root digest plus the [`SubtreeSource`] it was built from. The + /// subtree's contents are not materialized; when needed for building a witness + /// they are rebuilt on demand from the `SubtreeSource` (see + /// [`HashTree::witness`]). + stubs: Vec>, +} + +/// A reusable subtree collapsed to a single digest ("stub"), stored in a +/// [`NodeKind::Stub`] node. +/// +/// Holds one `Arc` (inside the [`SubtreeSource`]) plus a cheap [`Digest`], so it +/// can be stored inline, avoiding extra allocation and/or indirection. +#[derive(Clone, Debug)] +struct StubNode { + /// The subtree's root digest. Its contents are not materialized; they are + /// rebuilt on demand via [`SubtreeSource::expand`] during witness generation. + digest: Digest, + + /// The source that this stub was built from (paired with its expander), used + /// both to detect that an unchanged subtree can be reused from a baseline (by + /// source identity and certification version) and to rebuild it for witnesses. + /// Holds an `Arc` into the source, keeping it alive so the identity cannot be + /// recycled (no ABA) and the source stays available for expansion. + source: SubtreeSource, } impl HashTree { @@ -306,6 +369,7 @@ impl HashTree { node_labels: vec![Default::default()], node_children: vec![Default::default()], node_children_labels_ranges: vec![Default::default()], + stubs: vec![Default::default()], } } @@ -314,9 +378,12 @@ impl HashTree { let leaf_size: usize = self.leaf_digests.iter().map(|bucket| bucket.len()).sum(); let fork_size: usize = self.fork_digests.iter().map(|bucket| bucket.len()).sum(); let node_size: usize = self.node_digests.iter().map(|bucket| bucket.len()).sum(); + let stub_size: usize = self.stubs.iter().map(|bucket| bucket.len()).sum(); - // Since this is for metrics only we don't care about potential overflows - leaf_size + fork_size + node_size + // Since this is for metrics only we don't care about potential overflows. + // Note: each stub is counted as a single node; the nodes of its + // unmaterialized subtree are not counted here. + leaf_size + fork_size + node_size + stub_size } /// Largest index in the `HashTree`. @@ -339,8 +406,32 @@ impl HashTree { .map(|bucket| bucket.len()) .max() .unwrap_or(0); + let stub_size = self + .stubs + .iter() + .map(|bucket| bucket.len()) + .max() + .unwrap_or(0); - leaf_size.max(fork_size).max(node_size) + leaf_size.max(fork_size).max(node_size).max(stub_size) + } + + /// Number of [`NodeKind::Stub`] nodes in this tree. + /// + /// Diagnostics/test only. + #[doc(hidden)] + pub fn stub_count(&self) -> usize { + self.stubs.iter().map(|bucket| bucket.len()).sum() + } + + /// The [`SubtreeSource`] of every [`NodeKind::Stub`] node (in no particular + /// order). Lets tests assert stub source identity (e.g. that reuse is by + /// pointer, not by value). + /// + /// Diagnostics/test only. + #[doc(hidden)] + pub fn stub_sources(&self) -> impl Iterator { + self.stubs.iter().flatten().map(|stub| &stub.source) } /// Note that new forks are always added to fork_digests[0], but in order @@ -369,6 +460,13 @@ impl HashTree { NodeId::leaf(self.bucket_offset, id) } + /// Constructs a new stub (either freshly hashed or reused from a baseline). + fn new_stub(&mut self, digest: Digest, source: SubtreeSource) -> Result { + let idx = self.stubs[0].len(); + self.stubs[0].push(StubNode { digest, source }); + NodeId::stub(self.bucket_offset, idx) + } + /// Preallocates `len` nodes. Makes the new nodes root if the `parent` is /// `Empty`. Returns the [`NodeIndexRange`] to the allocated nodes. fn preallocate_nodes( @@ -429,6 +527,9 @@ impl HashTree { NodeKind::Leaf => { &self.leaf_digests[node_id.bucket() - self.bucket_offset][node_id.index()] } + NodeKind::Stub => { + &self.stubs[node_id.bucket() - self.bucket_offset][node_id.index()].digest + } NodeKind::Empty => &EMPTY_HASH, } } @@ -476,6 +577,7 @@ impl HashTree { self.node_children[bucket][idx], ), NodeKind::Leaf => HashTreeView::Leaf(&self.leaf_digests[bucket][idx]), + NodeKind::Stub => HashTreeView::Stub(&self.stubs[bucket][idx].digest), NodeKind::Empty => HashTreeView::Empty, } } @@ -486,6 +588,9 @@ impl HashTree { } /// Constructs a witness for the specified partial tree. + /// + /// Where the `partial_tree` descends into a [`NodeKind::Stub`] (e.g. into a + /// canister), the subtree is built on demand from its [`SubtreeSource`]. pub fn witness( &self, partial_tree: &LabeledTree>, @@ -623,6 +728,13 @@ impl HashTree { ); B::make_pruned(digest.clone()) } + HashTreeView::Stub(digest) => { + debug_assert!( + false, + "a tree node without children must not be a stub" + ); + B::make_pruned(digest.clone()) + } }); } @@ -666,6 +778,24 @@ impl HashTree { pos: NodeId, t: &LabeledTree>, ) -> Result> { + if pos.kind() == NodeKind::Stub { + // A stub, only storing its root digest. + return match t { + // Requested partial tree descends into the subtree: rebuild it from source and + // continue witness generation there. + LabeledTree::SubTree(children) if !children.is_empty() => { + let expanded = ht.stubs[pos.bucket()][pos.index()] + .source + .expand() + .expect("expanding a stub should not fail"); + go::(&expanded, NodeId::empty(), expanded.root, t) + } + + // Witness only needs the precomputed digest. + _ => Ok(B::make_pruned(ht.digest(pos).clone())), + }; + } + match t { LabeledTree::Leaf(data) => Ok(match ht.view(pos) { HashTreeView::Leaf(_) => B::make_leaf(&data[..]), @@ -674,12 +804,16 @@ impl HashTree { B::make_node(label.clone(), B::make_pruned(ht.digest(child).clone())) } HashTreeView::Fork(digest, _left, _right) => B::make_pruned(digest.clone()), + // Intercepted above; a stub behaves like an opaque subtree. + HashTreeView::Stub(digest) => B::make_pruned(digest.clone()), }), LabeledTree::SubTree(children) if children.is_empty() => Ok(match ht.view(pos) { HashTreeView::Empty => B::make_empty(), HashTreeView::Leaf(digest) => B::make_pruned(digest.clone()), HashTreeView::Fork(digest, _left, _right) => B::make_pruned(digest.clone()), HashTreeView::Node(digest, _label, _child) => B::make_pruned(digest.clone()), + // Intercepted above; a stub behaves like an opaque subtree. + HashTreeView::Stub(digest) => B::make_pruned(digest.clone()), }), LabeledTree::SubTree(children) => children .iter() @@ -710,6 +844,9 @@ impl HashTree { self.node_children.extend(subtree.node_children); self.node_children_labels_ranges .extend(subtree.node_children_labels_ranges); + + // Reusable stubs + self.stubs.extend(subtree.stubs); } } @@ -719,6 +856,10 @@ impl PartialEq for HashTree { fn eq_recursive(ht: &HashTree, ht_root: NodeId, other: &crypto::HashTree) -> bool { ht.digest(ht_root) == other.digest() && match (ht_root.kind(), other) { + // A stub collapses a whole subtree to its root digest, which the top-level + // digest comparison above already checked; there is no materialized structure + // left to compare. + (NodeKind::Stub, _) => true, (NodeKind::Leaf | NodeKind::Empty, crypto::HashTree::Leaf { digest: _ }) => { true } @@ -769,6 +910,8 @@ pub enum HashTreeView<'a> { Leaf(&'a Digest), Fork(&'a Digest, NodeId, NodeId), Node(&'a Digest, &'a Label, NodeId), + /// A subtree reduced to its root digest. + Stub(&'a Digest), } /// Error produced when computing hash trees @@ -780,9 +923,99 @@ pub enum HashTreeError { IndexOverflow, } +/// A cursor into a baseline [`HashTree`] that mirrors the position of the lazy +/// tree being traversed. Used to reuse subtrees with matching [`SubtreeSource`] +/// from a previously built tree, traversed in lockstep with the new tree. +#[derive(Clone, Copy)] +struct BaselineCursor<'a> { + tree: &'a HashTree, + /// The node at this position: `empty` for the root, otherwise the labeled + /// node (`kind() == Node`) reached via the edge leading here. + node: NodeId, +} + +impl<'a> BaselineCursor<'a> { + /// The subtree stored below `self.node` in the baseline tree. + fn subtree_root(&self) -> NodeId { + if self.node == NodeId::empty() { + self.tree.root + } else { + // Sanity check: a complete `HashTree` has no bucket offset. + debug_assert_eq!(self.tree.bucket_offset, 0); + + let bucket = self.node.bucket(); + self.tree.node_children[bucket][self.node.index()] + } + } + + /// If the baseline stored this position as a reusable [`NodeKind::Stub`], + /// returns the stub node. + fn stub(&self) -> Option<&'a StubNode> { + let subtree_root = self.subtree_root(); + if subtree_root.kind() == NodeKind::Stub { + // Sanity check: a complete `HashTree` has no bucket offset. + debug_assert_eq!(self.tree.bucket_offset, 0); + + Some(&self.tree.stubs[subtree_root.bucket()][subtree_root.index()]) + } else { + None + } + } + + /// Streams the children positions as `(label, cursor)` pairs, in label order. + fn children(self) -> impl Iterator)> + 'a { + let tree = self.tree; + let NodeIndexRange { + bucket, + index_range, + } = tree.node_labels_range(self.node); + index_range.map(move |idx| { + let child = NodeId::node(bucket, idx).expect("valid baseline hash tree"); + ( + &tree.node_labels[bucket][idx], + BaselineCursor { tree, node: child }, + ) + }) + } +} + /// Materializes the provided lazy tree and builds its hash tree that can be /// used to produce witnesses. +/// +/// Subtrees that carry a +/// [`LazyFork::subtree_source`](crate::lazy_tree::LazyFork::subtree_source) +/// (e.g. canisters) are collapsed to digest-only [`NodeKind::Stub`] nodes. +/// The resulting tree has the exact same root hash as a fully materialized +/// build; witnesses that descend into a stubbed subtree rebuild it on demand +/// from the [`SubtreeSource`] held in the stub (see [`HashTree::witness`]). pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { + hash_lazy_tree_impl(t, None) +} + +/// Like [`hash_lazy_tree`], but reuses the [`NodeKind::Stub`] nodes of +/// unchanged subtrees from `baseline`. +/// +/// The new lazy tree and the baseline tree are traversed in lockstep (children +/// merge-joined by label). Wherever a child carries a [`SubtreeSource`] equal +/// to the one the baseline stores under the same label the baseline's stored +/// digest is reused instead of building and hashing the subtree. +/// +/// The result is identical (same root hash, same witnesses) to a full +/// [`hash_lazy_tree`] build, regardless of `baseline`. In particular, a +/// `baseline` built under a different certification version is safe to pass: +/// its subtrees carry a different expander, so none of them are reused (they +/// are simply rebuilt). +pub fn hash_lazy_tree_with_baseline( + t: &LazyTree<'_>, + baseline: &HashTree, +) -> Result { + hash_lazy_tree_impl(t, Some(baseline)) +} + +fn hash_lazy_tree_impl( + t: &LazyTree<'_>, + baseline: Option<&HashTree>, +) -> Result { struct SubtreeRoot { children_range: NodeIndexRange, root: NodeId, @@ -814,12 +1047,76 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { } } - fn go( + /// Builds one labeled `child` of a fork (linked under `parent`), returning its + /// [`NodeId`] and whether it was expensively (re)built — i.e. materialized — + /// rather than cheaply reused from `baseline`. + /// + /// A `child` that carries a + /// [`LazyFork::subtree_source`](crate::lazy_tree::LazyFork::subtree_source) is + /// collapsed to a digest-only [`NodeKind::Stub`] — its digest reused from + /// `baseline` when the sources are equal (cheap), else rebuilt (expensive). + /// Any other `child` is materialized normally via [`build_tree`] (expensive). + fn build_child( + child: &LazyTree<'_>, + ht: &mut HashTree, + parent: NodeId, + par_strategy: &mut ParStrategy, + recursion_depth: u32, + baseline: Option>, + ) -> Result<(NodeId, bool), HashTreeError> { + if let LazyTree::LazyFork(f) = child + && let Some(source) = f.subtree_source() + { + // This subtree should be stubbed: store a digest-only [`NodeKind::Stub`]. + let (digest, was_built) = match baseline.and_then(|b| b.stub()) { + // Unchanged: the baseline carries an equal `SubtreeSource` — same source + // allocation *and* same expander (hence same certification version) — so its + // digest is reused (cheap). + Some(stub) if stub.source == source => (stub.digest.clone(), false), + + // New, changed, or built under a different version: build the subtree only to + // capture its root digest; if later needed for a witness, it will be rebuilt on + // demand from `source`. + _ => { + let mut child_ht = HashTree::new(); + child_ht.root = build_tree( + child, + &mut child_ht, + NodeId::empty(), + par_strategy, + recursion_depth + 1, + None, + )?; + child_ht.check_invariants(); + (child_ht.root_hash().clone(), true) + } + }; + return Ok((ht.new_stub(digest, source)?, was_built)); + } + + // Materialize non-stubbed child: expensive. + let id = build_tree( + child, + ht, + parent, + par_strategy, + recursion_depth + 1, + baseline, + )?; + Ok((id, true)) + } + + /// Builds the hash tree for `t`, returning the [`NodeId`] of its root. + /// + /// The hash tree is always materialized; collapsing a subtree fork into a + /// digest-only [`NodeKind::Stub`] happens one level up, in [`build_child`]. + fn build_tree( t: &LazyTree<'_>, ht: &mut HashTree, parent: NodeId, par_strategy: &mut ParStrategy, recursion_depth: u32, + baseline: Option>, ) -> Result { if recursion_depth > MAX_RECURSION_DEPTH { return Err(HashTreeError::RecursionTooDeep(MAX_RECURSION_DEPTH)); @@ -863,40 +1160,67 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { } = ht.preallocate_nodes(num_children, parent)?; let mut nodes = Vec::with_capacity(num_children); - // We only use multithreading if the number of children is large. It is generally - // efficient to do so because the children of a given parent are of the same type - // (e.g. everything under `/canisters` is a canister state) and thus require - // similar amounts of work to materialize. + // Build the children sequentially, but watch how many have to be actually built + // (hashed) rather than cheaply reused from the baseline. After a warmup, + // extrapolate that rate over the whole fork; if it projects too much work, hand + // the *remaining* children to the thread pool. This covers both stubbed forks + // (where reuse keeps the rate low) and regular forks (where every child is + // materialized; so a large fork always parallelizes). // - // We do not pass the thread pool down after use, so we are not spawning new threads - // in a nested way. - if num_children > 100 && par_strategy.is_concurrent() { - fork_parallel( + // We only materialize the unprocessed tail into a `Vec` if and when we + // switch; the common, all-sequential path stays fully lazy. + let may_parallelize = + num_children >= PARALLEL_MIN_CHILDREN && par_strategy.is_concurrent(); + let mut do_parallelize = may_parallelize && baseline.is_none(); + let mut num_processed = 0_usize; + let mut num_built = 0_usize; + + // Merge-join the children with the baseline children (a missing baseline child + // is `None`); each tagged with its preallocated node index. + let mut joined = range.zip(left_outer_join( + f.children(), + baseline.into_iter().flat_map(BaselineCursor::children), + )); + + while !do_parallelize && let Some((i, (label, child, base))) = joined.next() { + let (child, was_built) = build_child( + &child, + ht, + NodeId::node(bucket, i)?, + par_strategy, + recursion_depth, + base, + )?; + + num_built += was_built as usize; + num_processed += 1; + do_parallelize |= may_parallelize + // Beyond the warmup, switch to parallel once the sampled build rate + // (`num_built / num_processed`) projects more than the number of children + // required for parallel processing over all `num_children` (rearranged to avoid + // division). + && num_processed >= ADAPTIVE_WARMUP_CHILDREN + && num_built * num_children >= PARALLEL_MIN_CHILDREN * num_processed; + + let mut h = Hasher::for_domain("ic-hashtree-labeled"); + h.update(label.as_bytes()); + h.update(ht.digest(child).as_bytes()); + ht.node_digests[0][i] = h.finalize(); + ht.node_children[0][i] = child; + ht.node_labels[0][i] = label; + nodes.push(NodeId::node(bucket, i)?); + } + + // Build whatever is left of the children in parallel. + if do_parallelize { + build_fork_parallel( par_strategy.pool().unwrap(), ht, &mut nodes, - f, recursion_depth, bucket, - &range, + joined.collect(), )?; - } else { - for (i, (label, child)) in range.zip(f.children()) { - let child = go( - &child, - ht, - NodeId::node(bucket, i)?, - par_strategy, - recursion_depth + 1, - )?; - let mut h = Hasher::for_domain("ic-hashtree-labeled"); - h.update(label.as_bytes()); - h.update(ht.digest(child).as_bytes()); - ht.node_digests[0][i] = h.finalize(); - ht.node_children[0][i] = child; - ht.node_labels[0][i] = label; - nodes.push(NodeId::node(bucket, i)?); - } } if nodes.len() == 1 { @@ -928,21 +1252,26 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { } } - /// Does the same as the single-threaded else branch, but using multiple threads - fn fork_parallel( + /// Builds the given `tail` of a fork's children across the thread pool, + /// writing the resulting labeled nodes into `ht` and appending their + /// [`NodeId`]s to `nodes` (in `tail` order). + /// + /// Each `tail` entry is `(i, (label, child, base))`, where `i` is the child's + /// preallocated node index and `base` is its baseline counterpart (already + /// merge-joined by the caller). + #[allow(clippy::type_complexity)] + fn build_fork_parallel( thread_pool: &mut scoped_threadpool::Pool, ht: &mut HashTree, nodes: &mut Vec, - fork_f: &std::sync::Arc, depth: u32, bucket: usize, - range: &Range, + tail: Vec<(usize, (Label, LazyTree<'_>, Option>))>, ) -> Result<(), HashTreeError> { let bucket_offset = ht.node_children.len(); let threads = thread_pool.thread_count() as usize; - let children: Vec<_> = fork_f.children().collect(); debug_assert!(threads > 0); - let per_thread = ((children + let per_thread = ((tail .len() .checked_add(threads) .ok_or(HashTreeError::IndexOverflow)? @@ -953,9 +1282,10 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { let mut roots: Vec> = repeat_with(|| Vec::with_capacity(per_thread)) .take(threads) .collect(); + thread_pool.scoped(|scope| { for (i, (children, subtree, roots)) in izip!( - children.chunks(per_thread), + tail.chunks(per_thread), subtrees.iter_mut(), roots.iter_mut() ) @@ -970,21 +1300,32 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { // lookup based on NodeId. let mut ht = HashTree::new_with_bucket_offset(bucket_offset + i); let mut error: Option = None; - for (_, child) in children { + for (_i, (_label, child, base)) in children { // Since the parent is outside of `ht`, we set the parent to NodeId::empty() - // and fix the link from `root` to the parent later - let root = go( + // and fix the link from `root` to the parent later. A child that carries a + // `subtree_source` is collapsed to a stub here. + // + // A stub has no materialized labeled children of its own, so its + // `children_range` is empty (and is never consulted: stubs are descended into + // via their source during witness generation). + match build_child( child, &mut ht, NodeId::empty(), + // Run with `ParStrategy::Sequential`, so thread pools are never nested. &mut ParStrategy::Sequential, - depth + 1, - ); - match root { - Ok(root) => { + depth, + *base, + ) { + Ok((root, _was_built)) => { + let children_range = if root.kind() == NodeKind::Stub { + NodeIndexRange::default() + } else { + ht.root_labels_range.clone() + }; roots.push(SubtreeRoot { root, - children_range: ht.root_labels_range.clone(), + children_range, }); } Err(err) => { @@ -1004,7 +1345,8 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { for subtree in subtrees.into_iter().flatten() { ht.splice_subtree(subtree?); } - for (i, (label, _), root) in izip!(range.clone(), children, roots.into_iter().flatten()) { + for ((i, (label, _child, _base)), root) in tail.into_iter().zip(roots.into_iter().flatten()) + { ht.node_children_labels_ranges[bucket][i] = root.children_range; let mut h = Hasher::for_domain("ic-hashtree-labeled"); h.update(label.as_bytes()); @@ -1017,9 +1359,18 @@ pub fn hash_lazy_tree(t: &LazyTree<'_>) -> Result { Ok(()) } - let mut ht = HashTree::new(); - ht.root = go(t, &mut ht, NodeId::empty(), &mut ParStrategy::Concurrent, 0)?; + let baseline = baseline.map(|tree| BaselineCursor { + tree, + node: NodeId::empty(), + }); + let mut ht = HashTree::new(); + let strategy = &mut ParStrategy::Concurrent; + // The root is always materialized; only *descendants* that carry a + // `subtree_source` are collapsed into stubs (see `build_child`). Building a + // stand-alone subtree is just `hash_lazy_tree` on that subtree's root, which + // is in turn materialized for the same reason. + ht.root = build_tree(t, &mut ht, NodeId::empty(), strategy, 0, baseline)?; ht.check_invariants(); Ok(ht) diff --git a/rs/canonical_state/tree_hash/src/lazy_tree.rs b/rs/canonical_state/tree_hash/src/lazy_tree.rs index fd76d0a3cdc6..7cd4efd32dfd 100644 --- a/rs/canonical_state/tree_hash/src/lazy_tree.rs +++ b/rs/canonical_state/tree_hash/src/lazy_tree.rs @@ -8,6 +8,8 @@ pub mod materialize; use ic_crypto_tree_hash::Label; +use std::any::Any; +use std::fmt; use std::sync::Arc; /// A hash of the tree leaf contents according to the IC interface spec. See @@ -54,6 +56,22 @@ pub trait LazyFork<'a>: Send + Sync { fn is_empty(&self) -> bool { self.len() == 0 } + + /// The source that the subtree rooted at this fork is derived from, including + /// the [`SubtreeExpander`] that rebuilds it; produced iff the subtree should + /// be collapsed to a digest-only, reusable subtree node in the + /// [`HashTree`](crate::hash_tree::HashTree). + /// + /// Defaults to `None` (materialize the subtree inline). Forks that wrap shared, + /// copy-on-write state (e.g. an `Arc`) should override this to + /// return that `Arc`, together with an expander that bakes in the certification + /// version), as a [`SubtreeSource`]. Such subtrees are hashed once and, when an + /// unchanged subtree (same source) is found in a baseline tree, its digest is + /// reused instead of being recomputed. See + /// [`hash_lazy_tree_with_baseline`](crate::hash_tree::hash_lazy_tree_with_baseline). + fn subtree_source(&self) -> Option { + None + } } /// A tree that can lazily expand while it's being traversed. @@ -116,3 +134,94 @@ pub fn follow_path<'a>(t: &LazyTree<'a>, path: &[&[u8]]) -> Option> _ => None, } } + +/// An owned, type-erased handle to the source that a reusable lazy subtree was +/// derived from (e.g. an `Arc`), paired with the +/// [`SubtreeExpander`] that rebuilds the subtree from it. +/// +/// The held `Arc` keeps the source allocation alive, so its address cannot be +/// recycled for a different object while the handle exists (no ABA), and the +/// source stays available to [`expand`](Self::expand) the subtree for witnesses. +/// +/// Equality is a conservative reuse-gate, *not* a general-purpose comparison: +/// two `SubtreeSource`s are equal iff they point to the same source allocation +/// **and** carry the same expander. The expander encodes the producer's +/// certification version (baked into a version-specific monomorphization), so +/// equality implies the two subtrees would hash identically. The function +/// pointer comparison ([`std::ptr::fn_addr_eq`]) is best-effort: it may report +/// `false` for two pointers that are in fact the same function, but never `true` +/// for genuinely different ones. The sole consumer (baseline reuse) treats +/// inequality as "rebuild the subtree", so a false negative only costs a +/// recomputation and never compromises correctness. +#[derive(Clone)] +pub struct SubtreeSource { + source: Arc, + expander: SubtreeExpander, +} + +/// Rebuilds a stubbed subtree's [`HashTree`](crate::hash_tree::HashTree) from +/// its type-erased [`SubtreeSource`], by [downcasting](SubtreeSource::downcast) +/// the held `Arc` back to its concrete source and re-materializing it. Used to +/// expand a [`NodeKind::Stub`](crate::hash_tree::HashTree) on demand during +/// witness generation. +/// +/// It is a plain (non-capturing) function pointer, so the producer of the stub +/// must bake the certification version into it. The pointer alone fully +/// determines the expansion so it can be safely used as a conservative equality +/// gate for subtree reuse. +pub type SubtreeExpander = + fn(&SubtreeSource) -> Result; + +impl SubtreeSource { + /// Creates a handle that shares ownership of the subtree's `source` and can + /// rebuild the subtree from it, via the `expander`. + pub fn new(source: &Arc, expander: SubtreeExpander) -> Self { + let this = Self { + source: Arc::clone(source) as Arc, + expander, + }; + debug_assert!((expander)(&this).is_ok()); + this + } + + /// The bare address of the source allocation, used for identity comparison. + fn addr(&self) -> *const () { + Arc::as_ptr(&self.source) as *const () + } + + /// Recovers shared ownership of the source as an `Arc`. Used by a + /// [`SubtreeExpander`] to rebuild the subtree from its source. + /// + /// Panics if this handle was not created from an `Arc`. + pub fn downcast(&self) -> Arc { + Arc::clone(&self.source) + .downcast::() + .unwrap_or_else(|_| { + panic!( + "subtree source is not an Arc<{}>", + std::any::type_name::() + ) + }) + } + + /// Rebuilds the subtree's [`HashTree`](crate::hash_tree::HashTree) from this + /// source, to expand a stub on demand during witness generation. + pub fn expand(&self) -> Result { + (self.expander)(self) + } +} + +impl PartialEq for SubtreeSource { + /// A conservative, false-negative-only reuse-gate; see the type-level note. + fn eq(&self, other: &Self) -> bool { + self.addr() == other.addr() && std::ptr::fn_addr_eq(self.expander, other.expander) + } +} + +impl Eq for SubtreeSource {} + +impl fmt::Debug for SubtreeSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SubtreeSource({:p})", self.addr()) + } +} diff --git a/rs/canonical_state/tree_hash/tests/subtree.rs b/rs/canonical_state/tree_hash/tests/subtree.rs new file mode 100644 index 000000000000..96c4f3ad8d1b --- /dev/null +++ b/rs/canonical_state/tree_hash/tests/subtree.rs @@ -0,0 +1,433 @@ +//! Tests for reusable subtree (stub) nodes. +//! +//! When building a [`HashTree`] from a [`LazyTree`], every subtree that carries +//! a [`LazyFork::subtree_source`] (here a per-canister fork, mirroring `CanisterFork` +//! in production) is collapsed to a digest-only [`NodeKind::Stub`]. +//! Such a tree: +//! +//! * has the exact same root hash as a fully materialized build, +//! * serves witnesses by expanding the stub on demand from the source `Arc` +//! it holds (via its [`SubtreeExpander`]), with no external source, and +//! * when built with a baseline ([`hash_lazy_tree_with_baseline`]), reuses the +//! stored digest of every unchanged subtree (matched by `SubtreeSource`). + +use ic_canonical_state_tree_hash::hash_tree::{ + HashTree, HashTreeError, PARALLEL_MIN_CHILDREN, hash_lazy_tree, hash_lazy_tree_with_baseline, +}; +use ic_canonical_state_tree_hash::lazy_tree::{LazyFork, LazyTree, SubtreeSource, fork}; +use ic_canonical_state_tree_hash_test_utils::as_lazy; +use ic_crypto_tree_hash::{FlatMap, Label, LabeledTree, MixedHashTree, Witness, flatmap}; +use std::collections::BTreeMap; +use std::sync::Arc; + +const CANISTER_LABEL: &[u8] = b"canister"; +const TIME_LABEL: &[u8] = b"time"; + +/// Number of canisters; `> PARALLEL_MIN_CHILDREN` so that the parallel build +/// path is exercised. +const NUM_CANISTERS: usize = PARALLEL_MIN_CHILDREN * 2; + +const TIME: &[u8] = &[1, 2, 3, 4]; + +fn certified_data(i: usize) -> Vec { + vec![i as u8; 4] +} +fn controllers(i: usize) -> Vec { + format!("controllers-{i}").into_bytes() +} +fn custom_section(i: usize) -> Vec { + format!("section-{i}").into_bytes() +} +fn module_hash(i: usize) -> Vec { + vec![(i % 251) as u8; 32] +} + +fn canister_id_label(i: usize) -> Label { + Label::from(format!("{i:04}")) +} + +/// The certified subtree of a single canister (mirrors the real canonical +/// encoding: certified_data, controllers, metadata, module_hash). +fn canister_subtree(i: usize) -> LabeledTree> { + LabeledTree::SubTree(flatmap! { + Label::from("certified_data") => LabeledTree::Leaf(certified_data(i)), + Label::from("controllers") => LabeledTree::Leaf(controllers(i)), + Label::from("metadata") => LabeledTree::SubTree(flatmap!{ + Label::from("public_section") => LabeledTree::Leaf(custom_section(i)), + }), + Label::from("module_hash") => LabeledTree::Leaf(module_hash(i)), + }) +} + +/// A collection of canisters, each behind its own `Arc` (as in production). +type Canisters = BTreeMap>>>; + +fn canisters() -> Canisters { + (0..NUM_CANISTERS) + .map(|i| (canister_id_label(i), Arc::new(canister_subtree(i)))) + .collect() +} + +/// A `LazyFork` over the certified subtree of a single canister. +/// +/// `subtree_source` mirrors `CanisterFork::subtree_source` in production: it +/// returns the backing `Arc`, so each canister is stored as a self-contained, +/// reusable subtree node. +struct CanisterArcFork<'a> { + canister: &'a Arc>>, +} + +impl<'a> CanisterArcFork<'a> { + fn children_map(&self) -> &'a FlatMap>> { + match &**self.canister { + LabeledTree::SubTree(cs) => cs, + LabeledTree::Leaf(_) => panic!("a canister must be a subtree"), + } + } +} + +impl<'a> LazyFork<'a> for CanisterArcFork<'a> { + fn edge(&self, l: &Label) -> Option> { + self.children_map().get(l).map(as_lazy) + } + + fn labels(&self) -> Box + '_> { + Box::new(self.children_map().keys().iter().cloned()) + } + + fn children(&self) -> Box)> + 'a> { + Box::new( + self.children_map() + .iter() + .map(|(l, t)| (l.clone(), as_lazy(t))), + ) + } + + fn len(&self) -> usize { + self.children_map().len() + } + + fn subtree_source(&self) -> Option { + Some(SubtreeSource::new(self.canister, expand_test_canister)) + } +} + +/// Rebuilds a test canister's stubbed subtree from its `SubtreeSource` (mirrors +/// `expand_canister` in production, minus the certification version). +fn expand_test_canister(source: &SubtreeSource) -> Result { + let canister = source.downcast::>>(); + hash_lazy_tree(&canister_fork(&canister)) +} + +/// A `LazyFork` over the `/canister` subtree. +struct CanistersFork<'a> { + canisters: &'a Canisters, +} + +fn canister_fork(arc: &Arc>>) -> LazyTree<'_> { + fork(CanisterArcFork { canister: arc }) +} + +impl<'a> LazyFork<'a> for CanistersFork<'a> { + fn edge(&self, l: &Label) -> Option> { + self.canisters.get(l).map(canister_fork) + } + + fn labels(&self) -> Box + '_> { + Box::new(self.canisters.keys().cloned()) + } + + fn children(&self) -> Box)> + 'a> { + Box::new( + self.canisters + .iter() + .map(|(l, arc)| (l.clone(), canister_fork(arc))), + ) + } + + fn len(&self) -> usize { + self.canisters.len() + } +} + +/// The top-level state fork: `{canister: {...}, time: }`. +struct StateFork<'a> { + canisters: &'a Canisters, + time: &'a [u8], +} + +fn canisters_fork(canisters: &Canisters) -> LazyTree<'_> { + fork(CanistersFork { canisters }) +} + +impl<'a> LazyFork<'a> for StateFork<'a> { + fn edge(&self, l: &Label) -> Option> { + match l.as_bytes() { + CANISTER_LABEL => Some(canisters_fork(self.canisters)), + TIME_LABEL => Some(LazyTree::Blob(self.time, None)), + _ => None, + } + } + + fn labels(&self) -> Box + '_> { + Box::new([Label::from(CANISTER_LABEL), Label::from(TIME_LABEL)].into_iter()) + } + + fn children(&self) -> Box)> + 'a> { + Box::new( + [ + (Label::from(CANISTER_LABEL), canisters_fork(self.canisters)), + (Label::from(TIME_LABEL), LazyTree::Blob(self.time, None)), + ] + .into_iter(), + ) + } + + fn len(&self) -> usize { + 2 + } +} + +/// A `LazyTree` over the whole state. +fn state_tree<'a>(canisters: &'a Canisters, time: &'a [u8]) -> LazyTree<'a> { + fork(StateFork { canisters, time }) +} + +/// Asserts that `tree` produces exactly the same witness (both as +/// `MixedHashTree` and `Witness`) as the `reference` full build. Stubbed +/// subtrees expand themselves from the source `Arc` they hold. +fn assert_same_witness(reference: &HashTree, tree: &HashTree, partial: &LabeledTree>) { + let reference_mixed = reference + .witness::(partial) + .expect("reference MixedHashTree"); + let tree_mixed = tree + .witness::(partial) + .expect("MixedHashTree"); + assert_eq!( + reference_mixed, tree_mixed, + "MixedHashTree mismatch for partial {partial:?}" + ); + assert_eq!( + &tree_mixed.digest(), + reference.root_hash(), + "witness digest mismatch for partial {partial:?}" + ); + + let reference_witness = reference + .witness::(partial) + .expect("reference Witness"); + let tree_witness = tree.witness::(partial).expect("Witness"); + assert_eq!( + reference_witness, tree_witness, + "Witness mismatch for partial {partial:?}" + ); +} + +/// Builds a partial tree `{canister: {: inner}}`. +fn canister_query(i: usize, inner: LabeledTree>) -> LabeledTree> { + LabeledTree::SubTree(flatmap! { + Label::from(CANISTER_LABEL) => LabeledTree::SubTree(flatmap!{ + canister_id_label(i) => inner, + }), + }) +} + +/// The full canister subtree of canister `i`, used to request witnesses for +/// every leaf. +fn canister_partial(i: usize) -> LabeledTree> { + canister_query(i, canister_subtree(i)) +} + +#[test] +fn every_canister_is_a_subtree() { + let canisters = canisters(); + let tree = hash_lazy_tree(&state_tree(&canisters, TIME)).unwrap(); + + assert_eq!( + tree.stub_count(), + NUM_CANISTERS, + "every canister should be stored as a stub" + ); +} + +#[test] +fn witnesses_into_canisters_expand_from_source() { + let canisters = canisters(); + let source = state_tree(&canisters, TIME); + let tree = hash_lazy_tree(&source).unwrap(); + + // Whole-canister witnesses across both the sequential and parallel ranges. + for i in [ + 0usize, + 1, + PARALLEL_MIN_CHILDREN - 1, + PARALLEL_MIN_CHILDREN + 1, + NUM_CANISTERS - 1, + ] { + let partial = canister_partial(i); + let mixed = tree + .witness::(&partial) + .expect("witness expanded from source"); + assert_eq!(&mixed.digest(), tree.root_hash()); + // The requested leaves must be present (not pruned). + assert!( + mixed + .lookup(&[ + CANISTER_LABEL, + canister_id_label(i).as_bytes(), + b"module_hash" + ]) + .is_found(), + "expected canister {i} module_hash in the witness" + ); + } + + // A single leaf inside a canister. + let partial = canister_query( + 77, + LabeledTree::SubTree(flatmap! { + Label::from("module_hash") => LabeledTree::Leaf(module_hash(77)), + }), + ); + let mixed = tree.witness::(&partial).unwrap(); + assert_eq!(&mixed.digest(), tree.root_hash()); +} + +#[test] +fn absence_witnesses_expand_from_source() { + let canisters = canisters(); + let source = state_tree(&canisters, TIME); + let tree = hash_lazy_tree(&source).unwrap(); + + // Absent canister id (proven at the `/canister` node, no stub descent). + let partial = LabeledTree::SubTree(flatmap! { + Label::from(CANISTER_LABEL) => LabeledTree::SubTree(flatmap!{ + Label::from("zzzz") => LabeledTree::Leaf(vec![]), + }), + }); + let mixed = tree.witness::(&partial).unwrap(); + assert_eq!(&mixed.digest(), tree.root_hash()); + assert!( + mixed.lookup(&[CANISTER_LABEL, b"zzzz"]).is_absent(), + "expected absence proof, got {mixed:?}" + ); + + // Absent label *inside* a canister (descends into the subtree stub). + for i in [3usize, 110] { + let partial = canister_query( + i, + LabeledTree::SubTree(flatmap! { + Label::from("nonexistent") => LabeledTree::Leaf(vec![]), + }), + ); + let mixed = tree.witness::(&partial).unwrap(); + assert_eq!(&mixed.digest(), tree.root_hash()); + assert!( + mixed + .lookup(&[ + CANISTER_LABEL, + canister_id_label(i).as_bytes(), + b"nonexistent" + ]) + .is_absent(), + "expected absence proof inside canister {i}, got {mixed:?}" + ); + } +} + +/// Building with a baseline yields a tree identical to one built from scratch. +#[test] +fn baseline_build_matches_from_scratch() { + let canisters = canisters(); + let baseline = hash_lazy_tree(&state_tree(&canisters, TIME)).unwrap(); + + // Mutate a single canister (fresh `Arc`) and change `time`; keep the rest. + let mut next = canisters.clone(); + next.insert(canister_id_label(50), Arc::new(canister_subtree(9999))); + let new_time: &[u8] = &[9, 9, 9, 9]; + + let from_scratch = hash_lazy_tree(&state_tree(&next, new_time)).unwrap(); + let with_baseline = + hash_lazy_tree_with_baseline(&state_tree(&next, new_time), &baseline).unwrap(); + + assert_eq!( + from_scratch.root_hash(), + with_baseline.root_hash(), + "baseline build must have the same root hash as a from-scratch build" + ); + + // Witnesses must match between the two builds for the changed canister + // (whose contents are now those of `canister_subtree(9999)`), an unchanged + // canister, and the changed `time` leaf. Stubs expand themselves. + for partial in [ + canister_query(50, canister_subtree(9999)), + canister_partial(7), + LabeledTree::SubTree(flatmap! { + Label::from(TIME_LABEL) => LabeledTree::Leaf(new_time.to_vec()), + }), + ] { + assert_same_witness(&from_scratch, &with_baseline, &partial); + } +} + +/// Building with a baseline (reusing the stored digest of every unchanged +/// canister, rebuilding only the changed one) must produce exactly the same tree +/// as a from-scratch build. Digest reuse is an internal optimization and is not +/// observable in the result. +#[test] +fn baseline_build_with_partial_change_matches_from_scratch() { + let canisters = canisters(); + let baseline = hash_lazy_tree(&state_tree(&canisters, TIME)).unwrap(); + + // A `BTreeMap` clone shares the canister `Arc`s; only canister 50 gets a + // fresh `Arc` (a real mutation), so only it is rebuilt. + let mut next = canisters.clone(); + next.insert(canister_id_label(50), Arc::new(canister_subtree(50))); + + let with_baseline = hash_lazy_tree_with_baseline(&state_tree(&next, TIME), &baseline).unwrap(); + let from_scratch = hash_lazy_tree(&state_tree(&next, TIME)).unwrap(); + + assert_eq!(with_baseline.stub_count(), NUM_CANISTERS); + assert_eq!(with_baseline.root_hash(), from_scratch.root_hash()); +} + +/// Whether `a` and `b` hold the same stub [`SubtreeSource`]s by identity: each +/// canister's stub points to the same source `Arc` in both trees. +fn same_stub_sources(a: &HashTree, b: &HashTree) -> bool { + // Stubs are stored in label order in every tree + a.stub_sources().eq(b.stub_sources()) +} + +/// Whether every canister's stub in `a` references a *different* source `Arc` +/// than its counterpart in `b` (i.e. nothing could have been reused by identity). +fn disjoint_stub_sources(a: &HashTree, b: &HashTree) -> bool { + a.stub_count() == b.stub_count() && a.stub_sources().zip(b.stub_sources()).all(|(x, y)| x != y) +} + +/// Reuse is by identity, not by value: replacing every canister with a fresh +/// `Arc` of identical contents skips all digest reuse, yet still yields the same +/// root hash (the canonical encoding depends only on the contents). +#[test] +fn reuse_is_by_identity_not_by_value() { + let canisters = canisters(); + let baseline = hash_lazy_tree(&state_tree(&canisters, TIME)).unwrap(); + + // Rebuilding against the baseline with the *same* `Arc`s: every stub holds the + // very same source allocation as the baseline (identity is preserved). + let unchanged = hash_lazy_tree_with_baseline(&state_tree(&canisters, TIME), &baseline).unwrap(); + assert!(same_stub_sources(&baseline, &unchanged)); + + // Replace *every* canister with a fresh `Arc` of the same contents. + let next: Canisters = (0..NUM_CANISTERS) + .map(|i| (canister_id_label(i), Arc::new(canister_subtree(i)))) + .collect(); + + let with_baseline = hash_lazy_tree_with_baseline(&state_tree(&next, TIME), &baseline).unwrap(); + + // No stub shares a source `Arc` with the baseline, so no digest could have been + // reused (every `Arc` is fresh, so no identity matches). + assert!(disjoint_stub_sources(&baseline, &with_baseline)); + + // Same root hash nonetheless: the canonical encoding depends only on contents. + assert_eq!(with_baseline.root_hash(), baseline.root_hash()); +} diff --git a/rs/state_manager/benches/bench_traversal.rs b/rs/state_manager/benches/bench_traversal.rs index fe65023b79d9..2de20e716075 100644 --- a/rs/state_manager/benches/bench_traversal.rs +++ b/rs/state_manager/benches/bench_traversal.rs @@ -1,8 +1,8 @@ +use criterion::measurement::Measurement; use criterion::{BatchSize, BenchmarkId, Criterion, black_box}; -use criterion_time::ProcessTime; -use ic_base_types::NumBytes; +use ic_base_types::{NumBytes, NumSeconds}; use ic_canonical_state::{lazy_tree_conversion::replicated_state_as_lazy_tree, traverse}; -use ic_canonical_state_tree_hash::hash_tree::hash_lazy_tree; +use ic_canonical_state_tree_hash::hash_tree::{hash_lazy_tree, hash_lazy_tree_with_baseline}; use ic_canonical_state_tree_hash_test_utils::{build_witness_gen, crypto_hash_lazy_tree}; use ic_certification_version::CURRENT_CERTIFICATION_VERSION; use ic_crypto_tree_hash::{FlatMap, Label, LabeledTree, MixedHashTree, WitnessGenerator, flatmap}; @@ -15,7 +15,7 @@ use ic_replicated_state::{ }; use ic_state_manager::labeled_tree_visitor::LabeledTreeVisitor; use ic_state_manager::{stream_encoding::encode_stream_slice, tree_hash::hash_state}; -use ic_test_utilities_state::{get_initial_state, get_running_canister}; +use ic_test_utilities_state::{get_initial_state, new_canister_state_with_execution}; use ic_test_utilities_types::{ ids::{canister_test_id, message_test_id, subnet_test_id, user_test_id}, messages::{RequestBuilder, ResponseBuilder}, @@ -30,9 +30,9 @@ use ic_types_cycles::Cycles; use maplit::btreemap; use std::sync::Arc; -fn bench_traversal(c: &mut Criterion) { +fn bench_traversal(c: &mut Criterion) { const NUM_STREAM_MESSAGES: u64 = 1_000; - const NUM_CANISTERS: u64 = 10_000; + const NUM_CANISTERS: u64 = 500_000; const NUM_STATUSES: u64 = 30_000; let subnet_type = SubnetType::Application; @@ -72,7 +72,12 @@ fn bench_traversal(c: &mut Criterion) { }); for i in 0..NUM_CANISTERS { - state.put_canister_state(get_running_canister(canister_test_id(i))); + state.put_canister_state(new_canister_state_with_execution( + canister_test_id(i), + canister_test_id(i).get(), + Cycles::zero(), + NumSeconds::from(1000), + )); } let user_id = user_test_id(1); @@ -135,8 +140,25 @@ fn bench_traversal(c: &mut Criterion) { }); c.bench_function("traverse/hash_tree_new", |b| { + let mut tree = None; b.iter(|| { - black_box(hash_lazy_tree(&replicated_state_as_lazy_tree(&state, height)).unwrap()) + tree = Some(black_box( + hash_lazy_tree(&replicated_state_as_lazy_tree(&state, height)).unwrap(), + )); + }); + std::mem::drop(tree); + }); + + let baseline = hash_lazy_tree(&replicated_state_as_lazy_tree(&state, height)).unwrap(); + c.bench_function("traverse/hash_tree_cached", |b| { + b.iter(|| { + black_box( + hash_lazy_tree_with_baseline( + &replicated_state_as_lazy_tree(&state, height), + &baseline, + ) + .unwrap(), + ) }) }); @@ -278,10 +300,7 @@ fn bench_traversal(c: &mut Criterion) { } fn main() { - let mut c = Criterion::default() - .with_measurement(ProcessTime::UserTime) - .sample_size(20) - .configure_from_args(); + let mut c = Criterion::default().sample_size(20).configure_from_args(); bench_traversal(&mut c); c.final_summary(); } From 122281d74af27c6f5fbcd1eaf3491d997362bf90 Mon Sep 17 00:00:00 2001 From: Alin Sinpalean <58422065+alin-at-dfinity@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:43:41 +0200 Subject: [PATCH 74/75] Typo Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- rs/canonical_state/tree_hash/src/lazy_tree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/canonical_state/tree_hash/src/lazy_tree.rs b/rs/canonical_state/tree_hash/src/lazy_tree.rs index 7cd4efd32dfd..cc1158f4b552 100644 --- a/rs/canonical_state/tree_hash/src/lazy_tree.rs +++ b/rs/canonical_state/tree_hash/src/lazy_tree.rs @@ -65,7 +65,7 @@ pub trait LazyFork<'a>: Send + Sync { /// Defaults to `None` (materialize the subtree inline). Forks that wrap shared, /// copy-on-write state (e.g. an `Arc`) should override this to /// return that `Arc`, together with an expander that bakes in the certification - /// version), as a [`SubtreeSource`]. Such subtrees are hashed once and, when an + /// version, as a [`SubtreeSource`]. Such subtrees are hashed once and, when an /// unchanged subtree (same source) is found in a baseline tree, its digest is /// reused instead of being recomputed. See /// [`hash_lazy_tree_with_baseline`](crate::hash_tree::hash_lazy_tree_with_baseline). From 392145be9991417ae67a7d9523c1a25d3f16a033 Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Tue, 23 Jun 2026 19:37:18 +0000 Subject: [PATCH 75/75] Address review comments. --- rs/canonical_state/tree_hash/src/hash_tree.rs | 70 +++++++++++-------- rs/canonical_state/tree_hash/src/lazy_tree.rs | 4 +- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/rs/canonical_state/tree_hash/src/hash_tree.rs b/rs/canonical_state/tree_hash/src/hash_tree.rs index de0ab9ab7fa9..2aff6720e9da 100644 --- a/rs/canonical_state/tree_hash/src/hash_tree.rs +++ b/rs/canonical_state/tree_hash/src/hash_tree.rs @@ -21,12 +21,14 @@ const MAX_RECURSION_DEPTH: u32 = 128; /// built sequentially: it is too small for the thread pool to pay for itself. pub const PARALLEL_MIN_CHILDREN: usize = 1000; -/// When building against a baseline, a large fork starts building sequentially -/// and samples this many children before extrapolating, over the whole fork, the -/// rate at which they have to be *built* — i.e. hashed, rather than cheaply -/// reused from the baseline. With no baseline nothing can be reused, so the -/// switch to the thread pool happens immediately, without a warmup. -const ADAPTIVE_WARMUP_CHILDREN: usize = 1000; +/// Forks start being built sequentially. In the meantime, we track how many +/// children have been materialized and hashed — as opposed to cheaply reused +/// from the baseline. +/// +/// Once we've sampled at least this many children, if the projected number of +/// expensively built children exceeds `PARALLEL_MIN_CHILDREN`, we switch to +/// building in parallel. +const ADAPTIVE_WARMUP_CHILDREN: usize = 500; /// SHA256 of the domain separator "ic-hashtree-empty" const EMPTY_HASH: Digest = Digest([ @@ -333,8 +335,9 @@ pub struct HashTree { /// A reusable subtree collapsed to a single digest ("stub"), stored in a /// [`NodeKind::Stub`] node. /// -/// Holds one `Arc` (inside the [`SubtreeSource`]) plus a cheap [`Digest`], so it -/// can be stored inline, avoiding extra allocation and/or indirection. +/// Holds one `Arc` and one function pointer (inside the [`SubtreeSource`]) plus +/// a cheap [`Digest`], so it can be stored inline, avoiding extra allocation +/// and/or indirection. #[derive(Clone, Debug)] struct StubNode { /// The subtree's root digest. Its contents are not materialized; they are @@ -804,16 +807,16 @@ impl HashTree { B::make_node(label.clone(), B::make_pruned(ht.digest(child).clone())) } HashTreeView::Fork(digest, _left, _right) => B::make_pruned(digest.clone()), - // Intercepted above; a stub behaves like an opaque subtree. - HashTreeView::Stub(digest) => B::make_pruned(digest.clone()), + // Intercepted above. + HashTreeView::Stub(_) => unreachable!(), }), LabeledTree::SubTree(children) if children.is_empty() => Ok(match ht.view(pos) { HashTreeView::Empty => B::make_empty(), HashTreeView::Leaf(digest) => B::make_pruned(digest.clone()), HashTreeView::Fork(digest, _left, _right) => B::make_pruned(digest.clone()), HashTreeView::Node(digest, _label, _child) => B::make_pruned(digest.clone()), - // Intercepted above; a stub behaves like an opaque subtree. - HashTreeView::Stub(digest) => B::make_pruned(digest.clone()), + // Intercepted above. + HashTreeView::Stub(_) => unreachable!(), }), LabeledTree::SubTree(children) => children .iter() @@ -856,10 +859,15 @@ impl PartialEq for HashTree { fn eq_recursive(ht: &HashTree, ht_root: NodeId, other: &crypto::HashTree) -> bool { ht.digest(ht_root) == other.digest() && match (ht_root.kind(), other) { - // A stub collapses a whole subtree to its root digest, which the top-level - // digest comparison above already checked; there is no materialized structure - // left to compare. - (NodeKind::Stub, _) => true, + // A stub collapses a whole subtree to its root digest. Expand it from its + // source and compare the materialized subtree structurally. + (NodeKind::Stub, _) => { + let expanded = ht.stubs[ht_root.bucket()][ht_root.index()] + .source + .expand() + .expect("expanding a stub should not fail"); + eq_recursive(&expanded, expanded.root, other) + } (NodeKind::Leaf | NodeKind::Empty, crypto::HashTree::Leaf { digest: _ }) => { true } @@ -1022,13 +1030,13 @@ fn hash_lazy_tree_impl( } // We only initialize thread pools lazily the first time we need them - enum ParStrategy { + enum ParallelismStrategy { Sequential, Concurrent, ConcurrentInPool(scoped_threadpool::Pool), } - impl ParStrategy { + impl ParallelismStrategy { fn pool(&mut self) -> Option<&mut scoped_threadpool::Pool> { match self { Self::Sequential => None, @@ -1060,7 +1068,7 @@ fn hash_lazy_tree_impl( child: &LazyTree<'_>, ht: &mut HashTree, parent: NodeId, - par_strategy: &mut ParStrategy, + parallelism_strategy: &mut ParallelismStrategy, recursion_depth: u32, baseline: Option>, ) -> Result<(NodeId, bool), HashTreeError> { @@ -1083,7 +1091,7 @@ fn hash_lazy_tree_impl( child, &mut child_ht, NodeId::empty(), - par_strategy, + parallelism_strategy, recursion_depth + 1, None, )?; @@ -1099,7 +1107,7 @@ fn hash_lazy_tree_impl( child, ht, parent, - par_strategy, + parallelism_strategy, recursion_depth + 1, baseline, )?; @@ -1114,7 +1122,7 @@ fn hash_lazy_tree_impl( t: &LazyTree<'_>, ht: &mut HashTree, parent: NodeId, - par_strategy: &mut ParStrategy, + parallelism_strategy: &mut ParallelismStrategy, recursion_depth: u32, baseline: Option>, ) -> Result { @@ -1167,10 +1175,10 @@ fn hash_lazy_tree_impl( // (where reuse keeps the rate low) and regular forks (where every child is // materialized; so a large fork always parallelizes). // - // We only materialize the unprocessed tail into a `Vec` if and when we - // switch; the common, all-sequential path stays fully lazy. + // We only collect the unprocessed tail into a `Vec` if and when we switch; the + // common, all-sequential path uses the `joined` iterator directly. let may_parallelize = - num_children >= PARALLEL_MIN_CHILDREN && par_strategy.is_concurrent(); + num_children >= PARALLEL_MIN_CHILDREN && parallelism_strategy.is_concurrent(); let mut do_parallelize = may_parallelize && baseline.is_none(); let mut num_processed = 0_usize; let mut num_built = 0_usize; @@ -1187,7 +1195,7 @@ fn hash_lazy_tree_impl( &child, ht, NodeId::node(bucket, i)?, - par_strategy, + parallelism_strategy, recursion_depth, base, )?; @@ -1214,7 +1222,7 @@ fn hash_lazy_tree_impl( // Build whatever is left of the children in parallel. if do_parallelize { build_fork_parallel( - par_strategy.pool().unwrap(), + parallelism_strategy.pool().unwrap(), ht, &mut nodes, recursion_depth, @@ -1312,8 +1320,10 @@ fn hash_lazy_tree_impl( child, &mut ht, NodeId::empty(), - // Run with `ParStrategy::Sequential`, so thread pools are never nested. - &mut ParStrategy::Sequential, + // Run with `ParallelismStrategy::Sequential`: besides avoiding nested thread + // pools, this limits each worker's tree to a single bucket, which + // `splice_subtree` relies on to place worker `i` at bucket `bucket_offset + i`. + &mut ParallelismStrategy::Sequential, depth, *base, ) { @@ -1365,7 +1375,7 @@ fn hash_lazy_tree_impl( }); let mut ht = HashTree::new(); - let strategy = &mut ParStrategy::Concurrent; + let strategy = &mut ParallelismStrategy::Concurrent; // The root is always materialized; only *descendants* that carry a // `subtree_source` are collapsed into stubs (see `build_child`). Building a // stand-alone subtree is just `hash_lazy_tree` on that subtree's root, which diff --git a/rs/canonical_state/tree_hash/src/lazy_tree.rs b/rs/canonical_state/tree_hash/src/lazy_tree.rs index cc1158f4b552..bcbf77ebcb49 100644 --- a/rs/canonical_state/tree_hash/src/lazy_tree.rs +++ b/rs/canonical_state/tree_hash/src/lazy_tree.rs @@ -165,7 +165,7 @@ pub struct SubtreeSource { /// expand a [`NodeKind::Stub`](crate::hash_tree::HashTree) on demand during /// witness generation. /// -/// It is a plain (non-capturing) function pointer, so the producer of the stub +/// It is a plain function pointer (not a closure), so the producer of the stub /// must bake the certification version into it. The pointer alone fully /// determines the expansion so it can be safely used as a conservative equality /// gate for subtree reuse. @@ -180,7 +180,7 @@ impl SubtreeSource { source: Arc::clone(source) as Arc, expander, }; - debug_assert!((expander)(&this).is_ok()); + debug_assert!(expander(&this).is_ok()); this }