From ec42733021c4e45cc051e5aa8451fd5b2df4fa3f Mon Sep 17 00:00:00 2001 From: Adam Spofford <93943719+adamspofford-dfinity@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:15:16 -0700 Subject: [PATCH] feat: subnet metrics (#483) * Add subnet metrics API * Update dfx in CI * tests + test messages + fmt --- .github/workflows/ic-ref.yml | 2 +- CHANGELOG.md | 2 + .../agent/http_transport/hyper_transport.rs | 7 + .../agent/http_transport/reqwest_transport.rs | 7 + ic-agent/src/agent/mod.rs | 134 +++++++- ic-agent/src/agent/response_authentication.rs | 11 +- ic-transport-types/src/lib.rs | 55 ++++ ic-transport-types/src/request_id.rs | 8 +- ref-tests/tests/ic-ref.rs | 299 ++++++++++++------ 9 files changed, 406 insertions(+), 119 deletions(-) diff --git a/.github/workflows/ic-ref.yml b/.github/workflows/ic-ref.yml index 3a129e22..805acbda 100644 --- a/.github/workflows/ic-ref.yml +++ b/.github/workflows/ic-ref.yml @@ -28,7 +28,7 @@ jobs: - name: Install dfx uses: dfinity/setup-dfx@main with: - dfx-version: "0.15.1-beta.0" + dfx-version: "0.15.2-beta.0" - name: Cargo cache uses: actions/cache@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 70e41577..7bc1d453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +* Added `read_subnet_state_raw` to `Agent` and `read_subnet_state` to `Transport` for looking up raw state by subnet ID instead of canister ID. +* Added `read_state_subnet_metrics` to `Agent` to access subnet metrics, such as total spent cycles. * Types passed to the `to_request_id` function can now contain nested structs, signed integers, and externally tagged enums. * `Envelope` struct is public also outside of the crate. diff --git a/ic-agent/src/agent/http_transport/hyper_transport.rs b/ic-agent/src/agent/http_transport/hyper_transport.rs index 30035f01..05721806 100644 --- a/ic-agent/src/agent/http_transport/hyper_transport.rs +++ b/ic-agent/src/agent/http_transport/hyper_transport.rs @@ -260,6 +260,13 @@ where }) } + fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec) -> AgentFuture> { + Box::pin(async move { + let url = format!("{}/subnet/{subnet_id}/read_state", self.url); + self.request(Method::POST, url, Some(envelope)).await + }) + } + fn query(&self, effective_canister_id: Principal, envelope: Vec) -> AgentFuture> { Box::pin(async move { let url = format!("{}/canister/{effective_canister_id}/query", self.url); diff --git a/ic-agent/src/agent/http_transport/reqwest_transport.rs b/ic-agent/src/agent/http_transport/reqwest_transport.rs index 595f00b0..e0cbbd2f 100644 --- a/ic-agent/src/agent/http_transport/reqwest_transport.rs +++ b/ic-agent/src/agent/http_transport/reqwest_transport.rs @@ -193,6 +193,13 @@ impl Transport for ReqwestTransport { }) } + fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec) -> AgentFuture> { + Box::pin(async move { + let endpoint = format!("subnet/{subnet_id}/read_state"); + self.execute(Method::POST, &endpoint, Some(envelope)).await + }) + } + fn query(&self, effective_canister_id: Principal, envelope: Vec) -> AgentFuture> { Box::pin(async move { let endpoint = format!("canister/{effective_canister_id}/query"); diff --git a/ic-agent/src/agent/mod.rs b/ic-agent/src/agent/mod.rs index d7d45f99..c2a9def3 100644 --- a/ic-agent/src/agent/mod.rs +++ b/ic-agent/src/agent/mod.rs @@ -34,7 +34,7 @@ use backoff::{backoff::Backoff, ExponentialBackoffBuilder}; use ic_certification::{Certificate, Delegation, Label}; use ic_transport_types::{ signed::{SignedQuery, SignedRequestStatus, SignedUpdate}, - QueryResponse, ReadStateResponse, + QueryResponse, ReadStateResponse, SubnetMetrics, }; use serde::Serialize; use status::Status; @@ -49,6 +49,8 @@ use std::{ time::Duration, }; +use self::response_authentication::lookup_subnet_metrics; + const IC_STATE_ROOT_DOMAIN_SEPARATOR: &[u8; 14] = b"\x0Dic-state-root"; const IC_ROOT_KEY: &[u8; 133] = b"\x30\x81\x82\x30\x1d\x06\x0d\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x01\x02\x01\x06\x0c\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x02\x01\x03\x61\x00\x81\x4c\x0e\x6e\xc7\x1f\xab\x58\x3b\x08\xbd\x81\x37\x3c\x25\x5c\x3c\x37\x1b\x2e\x84\x86\x3c\x98\xa4\xf1\xe0\x8b\x74\x23\x5d\x14\xfb\x5d\x9c\x0c\xd5\x46\xd9\x68\x5f\x91\x3a\x0c\x0b\x2c\xc5\x34\x15\x83\xbf\x4b\x43\x92\xe4\x67\xdb\x96\xd6\x5b\x9b\xb4\xcb\x71\x71\x12\xf8\x47\x2e\x0d\x5a\x4d\x14\x50\x5f\xfd\x74\x84\xb0\x12\x91\x09\x1c\x5f\x87\xb9\x88\x83\x46\x3f\x98\x09\x1a\x0b\xaa\xae"; @@ -69,7 +71,7 @@ type AgentFuture<'a, V> = Pin> + ' /// /// Any error returned by these methods will bubble up to the code that called the [Agent]. pub trait Transport: Send + Sync { - /// Sends an asynchronous request to a Replica. The Request ID is non-mutable and + /// Sends an asynchronous request to a replica. The Request ID is non-mutable and /// depends on the content of the envelope. /// /// This normally corresponds to the `/api/v2/canister//call` endpoint. @@ -80,7 +82,7 @@ pub trait Transport: Send + Sync { request_id: RequestId, ) -> AgentFuture<()>; - /// Sends a synchronous request to a Replica. This call includes the body of the request message + /// Sends a synchronous request to a replica. This call includes the body of the request message /// itself (envelope). /// /// This normally corresponds to the `/api/v2/canister//read_state` endpoint. @@ -90,13 +92,19 @@ pub trait Transport: Send + Sync { envelope: Vec, ) -> AgentFuture>; - /// Sends a synchronous request to a Replica. This call includes the body of the request message + /// Sends a synchronous request to a replica. This call includes the body of the request message + /// itself (envelope). + /// + /// This normally corresponds to the `/api/v2/subnet//read_state` endpoint. + fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec) -> AgentFuture>; + + /// Sends a synchronous request to a replica. This call includes the body of the request message /// itself (envelope). /// /// This normally corresponds to the `/api/v2/canister//query` endpoint. fn query(&self, effective_canister_id: Principal, envelope: Vec) -> AgentFuture>; - /// Sends a status request to the Replica, returning whatever the replica returns. + /// Sends a status request to the replica, returning whatever the replica returns. /// In the current spec v2, this is a CBOR encoded status message, but we are not /// making this API attach semantics to the response. fn status(&self) -> AgentFuture>; @@ -124,6 +132,9 @@ impl Transport for Box { fn status(&self) -> AgentFuture> { (**self).status() } + fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec) -> AgentFuture> { + (**self).read_subnet_state(subnet_id, envelope) + } } impl Transport for Arc { fn call( @@ -147,6 +158,9 @@ impl Transport for Arc { fn status(&self) -> AgentFuture> { (**self).status() } + fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec) -> AgentFuture> { + (**self).read_subnet_state(subnet_id, envelope) + } } /// Classification of the result of a request_status_raw (poll) call. @@ -377,6 +391,21 @@ impl Agent { serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData) } + async fn read_subnet_state_endpoint( + &self, + subnet_id: Principal, + serialized_bytes: Vec, + ) -> Result + where + A: serde::de::DeserializeOwned, + { + let bytes = self + .transport + .read_subnet_state(subnet_id, serialized_bytes) + .await?; + serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData) + } + async fn call_endpoint( &self, effective_canister_id: Principal, @@ -584,7 +613,8 @@ impl Agent { } } - /// Request the raw state tree directly. See [the protocol docs](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-read-state) for more information. + /// Request the raw state tree directly, under an effective canister ID. + /// See [the protocol docs](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-read-state) for more information. pub async fn read_state_raw( &self, paths: Vec>, @@ -602,6 +632,25 @@ impl Agent { Ok(cert) } + /// Request the raw state tree directly, under a subnet ID. + /// See [the protocol docs](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-read-state) for more information. + pub async fn read_subnet_state_raw( + &self, + paths: Vec>, + subnet_id: Principal, + ) -> Result { + let content = self.read_state_content(paths)?; + let serialized_bytes = sign_envelope(&content, self.identity.clone())?; + + let read_state_response: ReadStateResponse = self + .read_subnet_state_endpoint(subnet_id, serialized_bytes) + .await?; + let cert: Certificate = serde_cbor::from_slice(&read_state_response.certificate) + .map_err(AgentError::InvalidCborData)?; + self.verify_for_subnet(&cert, subnet_id)?; + Ok(cert) + } + fn read_state_content(&self, paths: Vec>) -> Result { Ok(EnvelopeContent::ReadState { sender: self.identity.sender().map_err(AgentError::SigningError)?, @@ -631,6 +680,27 @@ impl Agent { .map_err(|_| AgentError::CertificateVerificationFailed()) } + /// Verify a certificate, checking delegation if present. + /// Only passes if the certificate is for the specified subnet. + pub fn verify_for_subnet( + &self, + cert: &Certificate, + subnet_id: Principal, + ) -> Result<(), AgentError> { + let sig = &cert.signature; + + let root_hash = cert.tree.digest(); + let mut msg = vec![]; + msg.extend_from_slice(IC_STATE_ROOT_DOMAIN_SEPARATOR); + msg.extend_from_slice(&root_hash); + + let der_key = self.check_delegation_for_subnet(&cert.delegation, subnet_id)?; + let key = extract_der(der_key)?; + + ic_verify_bls_signature::verify_bls_signature(sig, &msg, &key) + .map_err(|_| AgentError::CertificateVerificationFailed()) + } + fn check_delegation( &self, delegation: &Option, @@ -665,6 +735,30 @@ impl Agent { } } + fn check_delegation_for_subnet( + &self, + delegation: &Option, + subnet_id: Principal, + ) -> Result, AgentError> { + match delegation { + None => Ok(self.read_root_key()), + Some(delegation) => { + let cert = serde_cbor::from_slice(&delegation.certificate) + .map_err(AgentError::InvalidCborData)?; + self.verify_for_subnet(&cert, subnet_id)?; + let public_key_path = [ + "subnet".as_bytes(), + delegation.subnet_id.as_ref(), + "public_key".as_bytes(), + ]; + let pk = lookup_value(&cert, public_key_path) + .map_err(|_| AgentError::CertificateNotAuthorized())? + .to_vec(); + Ok(pk) + } + } + } + /// Request information about a particular canister for a single state subkey. /// See [the protocol docs](https://internetcomputer.org/docs/current/references/ic-interface-spec#state-tree-canister-information) for more information. pub async fn read_state_canister_info( @@ -701,6 +795,20 @@ impl Agent { lookup_canister_metadata(cert, canister_id, path) } + /// Request a list of metrics about the subnet. + pub async fn read_state_subnet_metrics( + &self, + subnet_id: Principal, + ) -> Result { + let paths = vec![vec![ + "subnet".into(), + Label::from_bytes(subnet_id.as_slice()), + "metrics".into(), + ]]; + let cert = self.read_subnet_state_raw(paths, subnet_id).await?; + lookup_subnet_metrics(cert, subnet_id) + } + /// Fetches the status of a particular request by its ID. pub async fn request_status_raw( &self, @@ -1111,8 +1219,11 @@ impl<'agent> QueryBuilder<'agent> { sender, canister_id, method_name, - arg - } = content else { unreachable!() }; + arg, + } = content + else { + unreachable!() + }; Ok(SignedQuery { ingress_expiry, sender, @@ -1264,8 +1375,11 @@ impl<'agent> UpdateBuilder<'agent> { sender, canister_id, method_name, - arg - } = content else { unreachable!() }; + arg, + } = content + else { + unreachable!() + }; Ok(SignedUpdate { nonce, ingress_expiry, diff --git a/ic-agent/src/agent/response_authentication.rs b/ic-agent/src/agent/response_authentication.rs index fa8aed65..52fa39c5 100644 --- a/ic-agent/src/agent/response_authentication.rs +++ b/ic-agent/src/agent/response_authentication.rs @@ -1,7 +1,7 @@ use crate::agent::{RejectCode, RejectResponse, RequestStatusResponse}; use crate::{export::Principal, AgentError, RequestId}; use ic_certification::{certificate::Certificate, hash_tree::Label, LookupResult}; -use ic_transport_types::ReplyResponse; +use ic_transport_types::{ReplyResponse, SubnetMetrics}; use std::str::from_utf8; const DER_PREFIX: &[u8; 37] = b"\x30\x81\x82\x30\x1d\x06\x0d\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x01\x02\x01\x06\x0c\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x02\x01\x03\x61\x00"; @@ -56,6 +56,15 @@ pub(crate) fn lookup_canister_metadata>( lookup_value(&certificate, path_canister).map(<[u8]>::to_vec) } +pub(crate) fn lookup_subnet_metrics>( + certificate: Certificate, + subnet_id: Principal, +) -> Result { + let path_stats = [b"subnet", subnet_id.as_slice(), b"metrics"]; + let metrics = lookup_value(&certificate, path_stats)?; + Ok(serde_cbor::from_slice(metrics)?) +} + pub(crate) fn lookup_request_status>( certificate: Certificate, request_id: &RequestId, diff --git a/ic-transport-types/src/lib.rs b/ic-transport-types/src/lib.rs index f8f44a41..a9a648c8 100644 --- a/ic-transport-types/src/lib.rs +++ b/ic-transport-types/src/lib.rs @@ -246,3 +246,58 @@ pub struct SignedDelegation { #[serde(with = "serde_bytes")] pub signature: Vec, } + +/// A list of subnet metrics. +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct SubnetMetrics { + /// The number of canisters on this subnet. + pub num_canisters: u64, + /// The total size of the state in bytes taken by canisters on this subnet since this subnet was created. + pub canister_state_bytes: u64, + /// The total number of cycles consumed by all current and deleted canisters on this subnet. + #[serde(with = "map_u128")] + pub consumed_cycles_total: u128, + /// The total number of transactions processed on this subnet since this subnet was created. + pub update_transactions_total: u64, +} + +mod map_u128 { + use serde::{ + de::{Error, IgnoredAny, MapAccess, Visitor}, + ser::SerializeMap, + Deserializer, Serializer, + }; + use std::fmt; + + pub fn serialize(val: &u128, s: S) -> Result { + let low = *val & u64::MAX as u128; + let high = *val >> 64; + let mut map = s.serialize_map(Some(2))?; + map.serialize_entry(&0, &low)?; + map.serialize_entry(&1, &(high != 0).then_some(high))?; + map.end() + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + d.deserialize_map(MapU128Visitor) + } + + struct MapU128Visitor; + + impl<'de> Visitor<'de> for MapU128Visitor { + type Value = u128; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a map of low and high") + } + + fn visit_map>(self, mut map: A) -> Result { + let (_, low): (IgnoredAny, u64) = map + .next_entry()? + .ok_or_else(|| A::Error::missing_field("0"))?; + let opt: Option<(IgnoredAny, Option)> = map.next_entry()?; + let high = opt.and_then(|x| x.1).unwrap_or(0); + Ok((high as u128) << 64 | low as u128) + } + } +} diff --git a/ic-transport-types/src/request_id.rs b/ic-transport-types/src/request_id.rs index 10a53858..11682ebb 100644 --- a/ic-transport-types/src/request_id.rs +++ b/ic-transport-types/src/request_id.rs @@ -455,7 +455,9 @@ impl SerializeStructVariant for StructVariantSerializer { SerializeStruct::serialize_field(&mut self.struct_ser, key, value) } fn end(self) -> Result { - let Some(inner_struct_hash) = SerializeStruct::end(self.struct_ser)? else { return Ok(None) }; + let Some(inner_struct_hash) = SerializeStruct::end(self.struct_ser)? else { + return Ok(None); + }; let outer_struct = StructSerializer { field_name: <_>::default(), fields: vec![(Sha256::digest(self.name).into(), inner_struct_hash)], @@ -482,7 +484,9 @@ impl SerializeTupleVariant for TupleVariantSerializer { SerializeSeq::serialize_element(&mut self.seq_ser, value) } fn end(self) -> Result { - let Some(inner_seq_hash) = SerializeSeq::end(self.seq_ser)? else { return Ok(None) }; + let Some(inner_seq_hash) = SerializeSeq::end(self.seq_ser)? else { + return Ok(None); + }; let outer_struct = StructSerializer { field_name: <_>::default(), fields: vec![(Sha256::digest(self.name).into(), inner_seq_hash)], diff --git a/ref-tests/tests/ic-ref.rs b/ref-tests/tests/ic-ref.rs index d36bc286..a8c98c77 100644 --- a/ref-tests/tests/ic-ref.rs +++ b/ref-tests/tests/ic-ref.rs @@ -55,11 +55,11 @@ mod management_canister { }, Argument, }; - use ref_tests::get_effective_canister_id; use ref_tests::{ create_agent, create_basic_identity, create_secp256k1_identity, with_agent, with_wallet_canister, }; + use ref_tests::{get_effective_canister_id, with_universal_canister}; use sha2::{Digest, Sha256}; use std::collections::HashSet; use std::convert::TryInto; @@ -420,22 +420,31 @@ mod management_canister { // Can't call update on a stopped canister let result = agent.update(&canister_id, "update").call_and_wait().await; assert!( - matches!(result, Err(AgentError::ReplicaError(RejectResponse{ - reject_code: RejectCode::CanisterError, - reject_message, - error_code: None, - })) if reject_message == format!("Canister {} is stopped", canister_id)) + matches!( + &result, + Err(AgentError::ReplicaError(RejectResponse { + reject_code: RejectCode::CanisterError, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("Canister {canister_id} is stopped") + && error_code == "IC0508" + ), + "wrong error: {result:?}" ); // Can't call query on a stopped canister let result = agent.query(&canister_id, "query").with_arg([]).call().await; assert!( - matches!(result, Err(AgentError::ReplicaError(RejectResponse{ - reject_code: RejectCode::CanisterError, - reject_message, - error_code: Some(ref error_code), - })) if reject_message == format!("IC0508: Canister {} is stopped and therefore does not have a CallContextManager", canister_id) && - error_code == "IC0508") + matches!( + &result, + Err(AgentError::ReplicaError(RejectResponse { + reject_code: RejectCode::CanisterError, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("IC0508: Canister {canister_id} is stopped and therefore does not have a CallContextManager") + && error_code == "IC0508" + ), + "wrong error: {result:?}" ); // Upgrade should succeed @@ -454,22 +463,30 @@ mod management_canister { // Can call update let result = agent.update(&canister_id, "update").call_and_wait().await; assert!( - matches!(result, Err(AgentError::ReplicaError(RejectResponse{ - reject_code: RejectCode::DestinationInvalid, - reject_message, - error_code: None, - })) if reject_message == format!("Canister {} has no update method 'update'", canister_id)) + matches!( + &result, + Err(AgentError::ReplicaError(RejectResponse { + reject_code: RejectCode::DestinationInvalid, + reject_message, + error_code: None, + })) if *reject_message == format!("Canister {canister_id} has no update method 'update'") + ), + "wrong error: {result:?}" ); // Can call query let result = agent.query(&canister_id, "query").with_arg([]).call().await; assert!( - matches!(result, Err(AgentError::ReplicaError(RejectResponse{ - reject_code: RejectCode::DestinationInvalid, - reject_message, - error_code: Some(ref error_code), - })) if reject_message == format!("IC0302: Canister {} has no query method 'query'", canister_id) && - error_code == "IC0302") + matches!( + &result, + Err(AgentError::ReplicaError(RejectResponse { + reject_code: RejectCode::DestinationInvalid, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("IC0302: Canister {} has no query method 'query'", canister_id) + && error_code == "IC0302", + ), + "wrong error: {result:?}" ); // Another start is a noop @@ -484,49 +501,65 @@ mod management_canister { // Cannot call update let result = agent.update(&canister_id, "update").call_and_wait().await; assert!( - matches!(result, Err(AgentError::ReplicaError(RejectResponse{ - reject_code: RejectCode::DestinationInvalid, - reject_message, - error_code: Some(ref error_code), - })) if reject_message == format!("Canister {} not found", canister_id) && - error_code == "IC0301") + matches!( + &result, + Err(AgentError::ReplicaError(RejectResponse { + reject_code: RejectCode::DestinationInvalid, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("Canister {} not found", canister_id) + && error_code == "IC0301" + ), + "wrong error: {result:?}" ); // Cannot call query let result = agent.query(&canister_id, "query").with_arg([]).call().await; assert!( - matches!(result, Err(AgentError::ReplicaError(RejectResponse{ - reject_code: RejectCode::DestinationInvalid, - reject_message, - error_code: Some(ref error_code) - })) if reject_message == format!("IC0301: Canister {} not found", canister_id) && - error_code == "IC0301") + matches!( + &result, + Err(AgentError::ReplicaError(RejectResponse { + reject_code: RejectCode::DestinationInvalid, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("IC0301: Canister {} not found", canister_id) + && error_code == "IC0301" + ), + "wrong error: {result:?}" ); // Cannot query canister status let result = ic00.canister_status(&canister_id).call_and_wait().await; - assert!(match result { - Err(AgentError::ReplicaError(RejectResponse{ - reject_code: RejectCode::DestinationInvalid, - reject_message, - error_code: Some(ref error_code) - })) - if reject_message == format!("Canister {} not found", canister_id) && - error_code == "IC0301" => - true, - Ok((_status_call_result,)) => false, - _ => false, - }); + assert!( + match &result { + Err(AgentError::ReplicaError(RejectResponse { + reject_code: RejectCode::DestinationInvalid, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("Canister {} not found", canister_id) + && error_code == "IC0301" => + { + true + } + Ok((_status_call_result,)) => false, + _ => false, + }, + "wrong error: {result:?}" + ); // Delete a deleted canister should fail. let result = ic00.delete_canister(&canister_id).call_and_wait().await; assert!( - matches!(result, Err(AgentError::ReplicaError(RejectResponse{ - reject_code: RejectCode::DestinationInvalid, - reject_message, - error_code: Some(ref error_code) - })) if reject_message == format!("Canister {} not found", canister_id) && - error_code == "IC0301") + matches!( + &result, + Err(AgentError::ReplicaError(RejectResponse{ + reject_code: RejectCode::DestinationInvalid, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("Canister {} not found", canister_id) + && error_code == "IC0301" + ), + "wrong error: {result:?}" ); Ok(()) }) @@ -562,24 +595,32 @@ mod management_canister { .start_canister(&canister_id) .call_and_wait() .await; - assert!(matches!(result, + assert!( + matches!( + &result, Err(AgentError::ReplicaError(RejectResponse { - reject_code: RejectCode::CanisterError, - reject_message, - error_code: Some(ref error_code) - })) if reject_message == format!("Only controllers of canister {} can call ic00 method start_canister", canister_id) && - error_code == "IC0512")); + reject_code: RejectCode::CanisterError, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("Only controllers of canister {} can call ic00 method start_canister", canister_id) + && error_code == "IC0512" + ), + "wrong error: {result:?}" + ); // Stop as a wrong controller should fail. let result = other_ic00.stop_canister(&canister_id).call_and_wait().await; assert!( - matches!(result, + matches!( + &result, Err(AgentError::ReplicaError(RejectResponse { - reject_code: RejectCode::CanisterError, - reject_message, - error_code: Some(ref error_code) - })) if reject_message == format!("Only controllers of canister {} can call ic00 method stop_canister", canister_id) && - error_code == "IC0512") + reject_code: RejectCode::CanisterError, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("Only controllers of canister {} can call ic00 method stop_canister", canister_id) + && error_code == "IC0512" + ), + "wrong error: {result:?}" ); // Get canister status as a wrong controller should fail. @@ -587,26 +628,36 @@ mod management_canister { .canister_status(&canister_id) .call_and_wait() .await; - assert!(matches!(result, + assert!( + matches!( + &result, Err(AgentError::ReplicaError(RejectResponse { - reject_code: RejectCode::CanisterError, - reject_message, - error_code: Some(ref error_code) - })) if reject_message == format!("Only controllers of canister {} can call ic00 method canister_status", canister_id) && - error_code == "IC0512")); + reject_code: RejectCode::CanisterError, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("Only controllers of canister {canister_id} can call ic00 method canister_status") + && error_code == "IC0512" + ), + "wrong error: {result:?}" + ); // Delete as a wrong controller should fail. let result = other_ic00 .delete_canister(&canister_id) .call_and_wait() .await; - assert!(matches!(result, + assert!( + matches!( + &result, Err(AgentError::ReplicaError(RejectResponse { - reject_code: RejectCode::CanisterError, - reject_message, - error_code: Some(ref error_code) - })) if reject_message == format!("Only controllers of canister {} can call ic00 method delete_canister", canister_id) && - error_code == "IC0512")); + reject_code: RejectCode::CanisterError, + reject_message, + error_code: Some(error_code), + })) if *reject_message == format!("Only controllers of canister {canister_id} can call ic00 method delete_canister") + && error_code == "IC0512" + ), + "wrong error: {result:?}" + ); Ok(()) }) @@ -663,7 +714,11 @@ mod management_canister { .call_and_wait() .await?; - assert!(result.cycles > 0_u64 && result.cycles < creation_fee); + assert!( + result.cycles > 0_u64 && result.cycles < creation_fee, + "expected 0..{creation_fee}, got {}", + result.cycles + ); let ic00 = ManagementCanister::create(&agent); // cycle balance is default_canister_balance when creating with @@ -692,7 +747,10 @@ mod management_canister { let result = ic00.canister_status(&canister_id_2).call_and_wait().await?; let cycles: i128 = result.0.cycles.0.try_into().unwrap(); let burned = amount as i128 - cycles; - assert!(burned > 0 && burned < 100_000_000); + assert!( + burned > 0 && burned < 100_000_000, + "expected 0..100_000_000, got {burned}" + ); Ok(()) }) @@ -754,6 +812,21 @@ mod management_canister { Ok(()) }) } + + #[ignore] + #[test] + fn subnet_metrics() { + with_universal_canister(|agent, _| async move { + let metrics = agent + .read_state_subnet_metrics(Principal::self_authenticating(&agent.read_root_key())) + .await?; + assert!( + metrics.num_canisters >= 1, + "expected universal canister in num_canisters" + ); + Ok(()) + }) + } } mod simple_calls { @@ -807,13 +880,16 @@ mod simple_calls { .call_and_wait() .await; - assert!(matches!( - result, - Err(AgentError::ReplicaError(RejectResponse { - reject_code: RejectCode::DestinationInvalid, - .. - })) - )); + assert!( + matches!( + &result, + Err(AgentError::ReplicaError(RejectResponse { + reject_code: RejectCode::DestinationInvalid, + .. + })), + ), + "wrong error: {result:?}" + ); Ok(()) }) } @@ -829,13 +905,16 @@ mod simple_calls { .call() .await; - assert!(matches!( - result, - Err(AgentError::ReplicaError(RejectResponse { - reject_code: RejectCode::DestinationInvalid, - .. - })) - )); + assert!( + matches!( + &result, + Err(AgentError::ReplicaError(RejectResponse { + reject_code: RejectCode::DestinationInvalid, + .. + })) + ), + "wrong error: {result:?}" + ); Ok(()) }) } @@ -897,14 +976,17 @@ mod extras { with_agent(|agent| async move { let ic00 = ManagementCanister::create(&agent); // Prevent creating with over 1 << 48. This does not contact the server. - assert!(ic00 + let result = ic00 .create_canister() .as_provisional_create_with_amount(None) .with_effective_canister_id(get_effective_canister_id()) .with_memory_allocation(1u64 << 50) .call_and_wait() - .await - .is_err()); + .await; + assert!( + result.is_err(), + "unexpected successful response: {result:?}" + ); let (_,) = ic00 .create_canister() @@ -944,15 +1026,17 @@ mod extras { fn freezing_threshold() { with_agent(|agent| async move { let ic00 = ManagementCanister::create(&agent); - - assert!(ic00 + let result = ic00 .create_canister() .as_provisional_create_with_amount(None) .with_effective_canister_id(get_effective_canister_id()) .with_freezing_threshold(2u128.pow(70)) .call_and_wait() - .await - .is_err()); + .await; + assert!( + result.is_err(), + "unexpected successful response: {result:?}" + ); Ok(()) }) @@ -1055,12 +1139,17 @@ mod extras { .call_and_wait() .await; - assert!(matches!(result, + assert!( + matches!( + &result, Err(AgentError::ReplicaError(RejectResponse { - reject_code: RejectCode::DestinationInvalid, - reject_message, - error_code: None, - })) if reject_message == "Canister iimsn-6yaaa-aaaaa-afiaa-cai is already installed")); + reject_code: RejectCode::DestinationInvalid, + reject_message, + error_code: None, + })) if reject_message == "Canister iimsn-6yaaa-aaaaa-afiaa-cai is already installed" + ), + "wrong error: {result:?}" + ); Ok(()) })