Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [EXC-1768] Add system API to get costs of management canister calls. #3584

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion rs/canister_sandbox/src/sandbox_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ mod tests {
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::{Global, NumWasmPages, PageIndex, PageMap};
use ic_replicated_state::{Global, NetworkTopology, NumWasmPages, PageIndex, PageMap};
use ic_system_api::{
sandbox_safe_system_state::{CanisterStatusView, SandboxSafeSystemState},
ApiType, ExecutionParameters, InstructionLimits,
Expand Down Expand Up @@ -242,6 +242,7 @@ mod tests {
caller,
0,
IS_WASM64_EXECUTION,
NetworkTopology::default(),
)
}

Expand Down
96 changes: 96 additions & 0 deletions rs/interfaces/src/execution_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,102 @@ pub trait SystemApi {
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()>;

/// Returns the replication factor of the provided subnet.
///
/// Traps if `src`/`size` are not a valid encoding of a principal, or if
/// the principal is not a subnet.
fn ic0_replication_factor(&self, src: usize, size: usize, heap: &[u8])
-> HypervisorResult<u32>;

/// This system call indicates the cycle cost of an inter-canister call,
/// i.e., `ic0.call_perform`.
///
/// The cost is determined by the byte length of the method name and the
/// length of the encoded payload.
///
/// The amount of cycles is represented by a 128-bit value and is copied
/// to the canister memory starting at the location `dst`.
fn ic0_cost_call(
&self,
method_name_size: u64,
payload_size: u64,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()>;

/// This system call indicates the cycle cost of creating a canister on
/// the same subnet, i.e., the management canister's `create_canister`.
///
/// The amount of cycles is represented by a 128-bit value and is copied
/// to the canister memory starting at the location `dst`.
fn ic0_cost_create_canister(&self, dst: usize, heap: &mut [u8]) -> HypervisorResult<()>;

/// This system call indicates the cycle cost of making an http outcall,
/// i.e., the management canister's `http_request`.
///
/// `request_size` is the sum of the lengths of the variable request parts, as
/// documented in the interface specification.
/// `max_res_bytes` is the maximum number of response bytes the caller wishes to
/// accept.
///
/// The amount of cycles is represented by a 128-bit value and is copied
/// to the canister memory starting at the location `dst`.
fn ic0_cost_http_request(
&self,
request_size: u64,
max_res_bytes: u64,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()>;

/// This system call indicates the cycle cost of signing with ecdsa,
/// i.e., the management canister's `sign_with_ecdsa`, for the key
/// with name given by `src` + `size`.
///
/// Traps if `src`/`size` cannot be decoded to a valid key name.
///
/// The amount of cycles is represented by a 128-bit value and is copied
/// to the canister memory starting at the location `dst`.
fn ic0_cost_ecdsa(
&self,
src: usize,
size: usize,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()>;

/// This system call indicates the cycle cost of signing with schnorr,
/// i.e., the management canister's `sign_with_schnorr` for the key
/// with name given by `src` + `size`.
///
/// Traps if `src`/`size` cannot be decoded to a valid key name.
///
/// The amount of cycles is represented by a 128-bit value and is copied
/// to the canister memory starting at the location `dst`.
fn ic0_cost_schnorr(
michael-weigelt marked this conversation as resolved.
Show resolved Hide resolved
&self,
src: usize,
size: usize,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()>;

/// This system call indicates the cycle cost of signing with ecdsa,
michael-weigelt marked this conversation as resolved.
Show resolved Hide resolved
/// i.e., the management canister's `vetkd_encrypted_key` for the key
michael-weigelt marked this conversation as resolved.
Show resolved Hide resolved
/// with name given by `src` + `size`.
///
/// Traps if `src`/`size` cannot be decoded to a valid key name.
///
/// The amount of cycles is represented by a 128-bit value and is copied
/// to the canister memory starting at the location `dst`.
fn ic0_cost_vetkey(
michael-weigelt marked this conversation as resolved.
Show resolved Hide resolved
&self,
src: usize,
size: usize,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()>;
}

#[derive(Copy, Clone, Eq, PartialEq, Debug)]
Expand Down
11 changes: 11 additions & 0 deletions rs/interfaces/src/execution_environment/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ pub enum HypervisorError {
bytes: NumBytes,
limit: NumBytes,
},
/// A user-specified Principal was not a valid subnet.
SubnetNotFound,
}

impl From<WasmInstrumentationError> for HypervisorError {
Expand All @@ -209,6 +211,9 @@ impl From<WasmEngineError> for HypervisorError {
impl std::fmt::Display for HypervisorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SubnetNotFound => {
write!(f, "User-specified Principal was not a valid subnet.")
}
Self::FunctionNotFound(table_idx, func_idx) => write!(
f,
"Canister requested to invoke a non-existent Wasm function {} from table {}",
Expand Down Expand Up @@ -401,6 +406,10 @@ impl AsErrorHelp for HypervisorError {
Self::FunctionNotFound(_, _)
| Self::ToolchainContractViolation { .. }
| Self::InvalidPrincipalId(_) => ErrorHelp::ToolchainError,
Self::SubnetNotFound => ErrorHelp::UserError {
suggestion: "Check the provided principal (not a subnet).".to_string(),
doc_link: doc_ref(""),
},
Self::MethodNotFound(_) => ErrorHelp::UserError {
suggestion: "Check that the method being called is exported by \
the target canister."
Expand Down Expand Up @@ -516,6 +525,7 @@ impl HypervisorError {
};

let code = match self {
Self::SubnetNotFound => E::SubnetNotFound,
Self::FunctionNotFound(_, _) => E::CanisterFunctionNotFound,
Self::MethodNotFound(_) => E::CanisterMethodNotFound,
Self::ToolchainContractViolation { .. } => E::CanisterContractViolation,
Expand Down Expand Up @@ -557,6 +567,7 @@ impl HypervisorError {
/// e.g. as a metric label.
pub fn as_str(&self) -> &'static str {
match self {
HypervisorError::SubnetNotFound => "SubnetNotFound",
HypervisorError::FunctionNotFound(..) => "FunctionNotFound",
HypervisorError::MethodNotFound(_) => "MethodNotFound",
HypervisorError::ToolchainContractViolation { .. } => "ToolchainContractViolation",
Expand Down
11 changes: 11 additions & 0 deletions rs/replicated_state/src/metadata_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use ic_registry_routing_table::{
};
use ic_registry_subnet_features::SubnetFeatures;
use ic_registry_subnet_type::SubnetType;
use ic_types::consensus::idkg::common::SignatureScheme;
use ic_types::{
batch::BlockmakerMetrics,
crypto::CryptoHash,
Expand Down Expand Up @@ -244,6 +245,16 @@ impl NetworkTopology {
.get(subnet_id)
.map(|subnet_topology| subnet_topology.nodes.len())
}

pub fn get_key_by_name(
&self,
signature_scheme: SignatureScheme,
key_name: &str,
) -> Option<&MasterPublicKeyId> {
self.chain_key_enabled_subnets
.keys()
.find(|&entry| signature_scheme == entry.into() && key_name == entry.get_key_name())
}
}

impl From<&NetworkTopology> for pb_metadata::NetworkTopology {
Expand Down
178 changes: 176 additions & 2 deletions rs/system_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ use ic_interfaces::execution_environment::{
use ic_logger::{error, ReplicaLogger};
use ic_registry_subnet_type::SubnetType;
use ic_replicated_state::{
canister_state::WASM_PAGE_SIZE_IN_BYTES, memory_required_to_push_request, Memory, NumWasmPages,
PageIndex,
canister_state::WASM_PAGE_SIZE_IN_BYTES, memory_required_to_push_request, Memory,
NetworkTopology, NumWasmPages, PageIndex,
};
use ic_sys::PageBytes;
use ic_types::{
consensus::idkg::common::SignatureScheme,
ingress::WasmResult,
messages::{CallContextId, RejectContext, Request, MAX_INTER_CANISTER_PAYLOAD_IN_BYTES},
methods::{SystemMethod, WasmClosure},
Expand All @@ -36,6 +37,7 @@ use std::{
collections::BTreeMap,
convert::{From, TryFrom},
rc::Rc,
str,
};

pub mod cycles_balance_change;
Expand Down Expand Up @@ -3541,6 +3543,178 @@ impl SystemApi for SystemApiImpl {
trace_syscall!(self, CyclesBurn128, result, amount);
result
}

fn ic0_replication_factor(
&self,
src: usize,
size: usize,
heap: &[u8],
) -> HypervisorResult<u32> {
let msg_bytes = valid_subslice("ic0_replication_factor", src, size, heap)?;
let subnet_id = PrincipalId::try_from(msg_bytes)
.map_err(|e| HypervisorError::InvalidPrincipalId(PrincipalIdBlobParseError(e.0)))?;
self.sandbox_safe_system_state
.network_topology
.get_subnet_size(&subnet_id.into())
.map(|x| x as u32)
.ok_or(HypervisorError::SubnetNotFound)
}
michael-weigelt marked this conversation as resolved.
Show resolved Hide resolved

fn ic0_cost_call(
&self,
method_name_size: u64,
payload_size: u64,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()> {
let subnet_size = self.sandbox_safe_system_state.subnet_size;
let cost = self
.sandbox_safe_system_state
.cycles_account_manager
.xnet_call_performed_fee(subnet_size)
+ self
.sandbox_safe_system_state
.cycles_account_manager
.xnet_call_bytes_transmitted_fee(
(method_name_size + payload_size).into(),
subnet_size,
);
copy_cycles_to_heap(cost, dst, heap, "ic0_cost_call")?;
Ok(())
}

fn ic0_cost_create_canister(&self, dst: usize, heap: &mut [u8]) -> HypervisorResult<()> {
let subnet_size = self.sandbox_safe_system_state.subnet_size;
let cost = self
.sandbox_safe_system_state
.cycles_account_manager
.canister_creation_fee(subnet_size);
copy_cycles_to_heap(cost, dst, heap, "ic0_cost_create_canister")?;
Ok(())
}

fn ic0_cost_http_request(
&self,
request_size: u64,
max_res_bytes: u64,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()> {
let subnet_size = self.sandbox_safe_system_state.subnet_size;
let cost = self
.sandbox_safe_system_state
.cycles_account_manager
.http_request_fee(request_size.into(), Some(max_res_bytes.into()), subnet_size);
copy_cycles_to_heap(cost, dst, heap, "ic0_cost_http_request")?;
Ok(())
}

fn ic0_cost_ecdsa(
&self,
src: usize,
size: usize,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()> {
let topology = &self.sandbox_safe_system_state.network_topology;
let subnet_size = get_signing_key_replication_factor(
SignatureScheme::Ecdsa,
"ecdsa",
topology,
src,
size,
heap,
)?;
let cost = self
.sandbox_safe_system_state
.cycles_account_manager
.ecdsa_signature_fee(subnet_size);
copy_cycles_to_heap(cost, dst, heap, "ic0_cost_ecdsa")?;
Ok(())
}

fn ic0_cost_schnorr(
&self,
src: usize,
size: usize,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()> {
let topology = &self.sandbox_safe_system_state.network_topology;
let subnet_size = get_signing_key_replication_factor(
SignatureScheme::Schnorr,
"schnorr",
topology,
src,
size,
heap,
)?;
let cost = self
.sandbox_safe_system_state
.cycles_account_manager
.schnorr_signature_fee(subnet_size);
copy_cycles_to_heap(cost, dst, heap, "ic0_cost_schnorr")?;
Ok(())
}

fn ic0_cost_vetkey(
&self,
src: usize,
size: usize,
dst: usize,
heap: &mut [u8],
) -> HypervisorResult<()> {
let topology = &self.sandbox_safe_system_state.network_topology;
let subnet_size = get_signing_key_replication_factor(
SignatureScheme::VetKd,
"vetkey",
topology,
src,
size,
heap,
)?;
let cost = self
.sandbox_safe_system_state
.cycles_account_manager
.vetkd_fee(subnet_size);
copy_cycles_to_heap(cost, dst, heap, "ic0_cost_vetkey")?;
Ok(())
}
}

/// Common steps for the syscalls `ic0_cost_ecdsa`, `ic0_cost_schnorr` and `ic0_cost_vetkey`.
/// Extract the key name, look it up in `chain_key_enabled_subnets`, then extract all subnets
/// for that key and return the replication factor of the biggest one.
fn get_signing_key_replication_factor(
scheme: SignatureScheme,
scheme_str: &str,
topology: &NetworkTopology,
src: usize,
size: usize,
heap: &mut [u8],
) -> HypervisorResult<usize> {
let key_bytes = valid_subslice(&format!("ic0.cost_{} heap", scheme_str), src, size, heap)?;
let key_name =
str::from_utf8(key_bytes).map_err(|_| HypervisorError::ToolchainContractViolation {
error: format!(
"Failed to decode key name {}",
String::from_utf8_lossy(key_bytes)
),
})?;
let key_id = topology.get_key_by_name(scheme, key_name).ok_or(
HypervisorError::ToolchainContractViolation {
error: format!("{} signing key {} not known.", scheme, key_name),
},
)?;
let max_subnet_size = topology
.chain_key_enabled_subnets(key_id)
.iter() // this is non-empty, otherwise key_id would have errored
.map(|subnet_id| {
topology.get_subnet_size(subnet_id).unwrap() // we got the subnet_id from the collection
})
.max()
.unwrap(); // the maximum of a non-empty sequence of usize exists
Ok(max_subnet_size)
}

/// The default implementation of the `OutOfInstructionHandler` trait.
Expand Down
Loading
Loading