Skip to content

Commit

Permalink
feat: subnet metrics (#483)
Browse files Browse the repository at this point in the history
* Add subnet metrics API

* Update dfx in CI

* tests + test messages + fmt
  • Loading branch information
adamspofford-dfinity authored Oct 31, 2023
1 parent 3a4cc37 commit ec42733
Show file tree
Hide file tree
Showing 9 changed files with 406 additions and 119 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ic-ref.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions ic-agent/src/agent/http_transport/hyper_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,13 @@ where
})
}

fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>> {
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<u8>) -> AgentFuture<Vec<u8>> {
Box::pin(async move {
let url = format!("{}/canister/{effective_canister_id}/query", self.url);
Expand Down
7 changes: 7 additions & 0 deletions ic-agent/src/agent/http_transport/reqwest_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ impl Transport for ReqwestTransport {
})
}

fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>> {
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<u8>) -> AgentFuture<Vec<u8>> {
Box::pin(async move {
let endpoint = format!("canister/{effective_canister_id}/query");
Expand Down
134 changes: 124 additions & 10 deletions ic-agent/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand All @@ -69,7 +71,7 @@ type AgentFuture<'a, V> = Pin<Box<dyn Future<Output = Result<V, AgentError>> + '
///
/// 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/<effective_canister_id>/call` endpoint.
Expand All @@ -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/<effective_canister_id>/read_state` endpoint.
Expand All @@ -90,13 +92,19 @@ pub trait Transport: Send + Sync {
envelope: Vec<u8>,
) -> AgentFuture<Vec<u8>>;

/// 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/<subnet_id>/read_state` endpoint.
fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>>;

/// 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/<effective_canister_id>/query` endpoint.
fn query(&self, effective_canister_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>>;

/// 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<Vec<u8>>;
Expand Down Expand Up @@ -124,6 +132,9 @@ impl<I: Transport + ?Sized> Transport for Box<I> {
fn status(&self) -> AgentFuture<Vec<u8>> {
(**self).status()
}
fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>> {
(**self).read_subnet_state(subnet_id, envelope)
}
}
impl<I: Transport + ?Sized> Transport for Arc<I> {
fn call(
Expand All @@ -147,6 +158,9 @@ impl<I: Transport + ?Sized> Transport for Arc<I> {
fn status(&self) -> AgentFuture<Vec<u8>> {
(**self).status()
}
fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>> {
(**self).read_subnet_state(subnet_id, envelope)
}
}

/// Classification of the result of a request_status_raw (poll) call.
Expand Down Expand Up @@ -377,6 +391,21 @@ impl Agent {
serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData)
}

async fn read_subnet_state_endpoint<A>(
&self,
subnet_id: Principal,
serialized_bytes: Vec<u8>,
) -> Result<A, AgentError>
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,
Expand Down Expand Up @@ -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<Vec<Label>>,
Expand All @@ -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<Vec<Label>>,
subnet_id: Principal,
) -> Result<Certificate, AgentError> {
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<Vec<Label>>) -> Result<EnvelopeContent, AgentError> {
Ok(EnvelopeContent::ReadState {
sender: self.identity.sender().map_err(AgentError::SigningError)?,
Expand Down Expand Up @@ -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<Delegation>,
Expand Down Expand Up @@ -665,6 +735,30 @@ impl Agent {
}
}

fn check_delegation_for_subnet(
&self,
delegation: &Option<Delegation>,
subnet_id: Principal,
) -> Result<Vec<u8>, 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(
Expand Down Expand Up @@ -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<SubnetMetrics, AgentError> {
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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 10 additions & 1 deletion ic-agent/src/agent/response_authentication.rs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -56,6 +56,15 @@ pub(crate) fn lookup_canister_metadata<Storage: AsRef<[u8]>>(
lookup_value(&certificate, path_canister).map(<[u8]>::to_vec)
}

pub(crate) fn lookup_subnet_metrics<Storage: AsRef<[u8]>>(
certificate: Certificate<Storage>,
subnet_id: Principal,
) -> Result<SubnetMetrics, AgentError> {
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<Storage: AsRef<[u8]>>(
certificate: Certificate<Storage>,
request_id: &RequestId,
Expand Down
55 changes: 55 additions & 0 deletions ic-transport-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,58 @@ pub struct SignedDelegation {
#[serde(with = "serde_bytes")]
pub signature: Vec<u8>,
}

/// 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<S: Serializer>(val: &u128, s: S) -> Result<S::Ok, S::Error> {
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<u128, D::Error> {
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<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let (_, low): (IgnoredAny, u64) = map
.next_entry()?
.ok_or_else(|| A::Error::missing_field("0"))?;
let opt: Option<(IgnoredAny, Option<u64>)> = map.next_entry()?;
let high = opt.and_then(|x| x.1).unwrap_or(0);
Ok((high as u128) << 64 | low as u128)
}
}
}
8 changes: 6 additions & 2 deletions ic-transport-types/src/request_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,9 @@ impl SerializeStructVariant for StructVariantSerializer {
SerializeStruct::serialize_field(&mut self.struct_ser, key, value)
}
fn end(self) -> Result<Self::Ok, Self::Error> {
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)],
Expand All @@ -482,7 +484,9 @@ impl SerializeTupleVariant for TupleVariantSerializer {
SerializeSeq::serialize_element(&mut self.seq_ser, value)
}
fn end(self) -> Result<Self::Ok, Self::Error> {
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)],
Expand Down
Loading

0 comments on commit ec42733

Please sign in to comment.