Skip to content

Commit

Permalink
feat: add new operation to PocketIC to get canister controllers (#2495)
Browse files Browse the repository at this point in the history
This PR adds a new PocketIC operation to get the controllers of a
canister.
  • Loading branch information
mraszyk authored Nov 11, 2024
1 parent 14093af commit c31d549
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 13 deletions.
1 change: 1 addition & 0 deletions packages/pocket-ic/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The function `PocketIcBuilder::new_with_config` to specify a custom `ExtendedSubnetConfigSet`.
- The function `PocketIcBuilder::with_subnet_state` to load subnet state from a state directory for an arbitrary subnet kind and subnet id.
- The function `get_default_effective_canister_id` to retrieve a default effective canister id for canister creation on a PocketIC instance.
- The function `PocketIc::get_controllers` to get the controllers of a canister.

### Removed
- Functions `PocketIc::from_config`, `PocketIc::from_config_and_max_request_time`, and `PocketIc::from_config_and_server_url`.
Expand Down
22 changes: 22 additions & 0 deletions packages/pocket-ic/src/common/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,28 @@ pub struct RawCycles {
pub cycles: u128,
}

#[derive(Clone, Serialize, Eq, PartialEq, Ord, PartialOrd, Deserialize, Debug, JsonSchema)]
pub struct RawPrincipalId {
// raw bytes of the principal
#[serde(deserialize_with = "base64::deserialize")]
#[serde(serialize_with = "base64::serialize")]
pub principal_id: Vec<u8>,
}

impl From<Principal> for RawPrincipalId {
fn from(principal: Principal) -> Self {
Self {
principal_id: principal.as_slice().to_vec(),
}
}
}

impl From<RawPrincipalId> for Principal {
fn from(raw_principal_id: RawPrincipalId) -> Self {
Principal::from_slice(&raw_principal_id.principal_id)
}
}

#[derive(Clone, Serialize, Eq, PartialEq, Ord, PartialOrd, Deserialize, Debug, JsonSchema)]
pub struct RawCanisterId {
// raw bytes of the principal
Expand Down
8 changes: 8 additions & 0 deletions packages/pocket-ic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,14 @@ impl PocketIc {
runtime.block_on(async { self.pocket_ic.advance_time(duration).await })
}

/// Get the controllers of a canister.
/// Panics if the canister does not exist.
#[instrument(ret, skip(self), fields(instance_id=self.pocket_ic.instance_id, canister_id = %canister_id.to_string()))]
pub fn get_controllers(&self, canister_id: CanisterId) -> Vec<Principal> {
let runtime = self.runtime.clone();
runtime.block_on(async { self.pocket_ic.get_controllers(canister_id).await })
}

/// Get the current cycles balance of a canister.
#[instrument(ret, skip(self), fields(instance_id=self.pocket_ic.instance_id, canister_id = %canister_id.to_string()))]
pub fn cycle_balance(&self, canister_id: CanisterId) -> u128 {
Expand Down
20 changes: 18 additions & 2 deletions packages/pocket-ic/src/nonblocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::common::rest::{
HttpGatewayConfig, HttpGatewayInfo, HttpsConfig, InstanceConfig, InstanceId,
MockCanisterHttpResponse, RawAddCycles, RawCanisterCall, RawCanisterHttpRequest, RawCanisterId,
RawCanisterResult, RawCycles, RawEffectivePrincipal, RawMessageId, RawMockCanisterHttpResponse,
RawSetStableMemory, RawStableMemory, RawSubmitIngressResult, RawSubnetId, RawTime,
RawVerifyCanisterSigArg, RawWasmResult, SubnetId, Topology,
RawPrincipalId, RawSetStableMemory, RawStableMemory, RawSubmitIngressResult, RawSubnetId,
RawTime, RawVerifyCanisterSigArg, RawWasmResult, SubnetId, Topology,
};
use crate::management_canister::{
CanisterId, CanisterIdRecord, CanisterInstallMode, CanisterInstallModeUpgradeInner,
Expand Down Expand Up @@ -456,6 +456,22 @@ impl PocketIc {
self.set_time(now + duration).await;
}

/// Get the controllers of a canister.
/// Panics if the canister does not exist.
#[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
pub async fn get_controllers(&self, canister_id: CanisterId) -> Vec<Principal> {
let endpoint = "read/get_controllers";
let result: Vec<RawPrincipalId> = self
.post(
endpoint,
RawCanisterId {
canister_id: canister_id.as_slice().to_vec(),
},
)
.await;
result.into_iter().map(|p| p.into()).collect()
}

/// Get the current cycles balance of a canister.
#[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
pub async fn cycle_balance(&self, canister_id: CanisterId) -> u128 {
Expand Down
32 changes: 32 additions & 0 deletions packages/pocket-ic/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1595,3 +1595,35 @@ fn test_get_default_effective_canister_id_invalid_url() {
err => panic!("Unexpected error: {}", err),
};
}

#[test]
fn get_controllers() {
let pic = PocketIc::new();

let canister_id = pic.create_canister();

let controllers = pic.get_controllers(canister_id);
assert_eq!(controllers, vec![Principal::anonymous()]);

let user_id = Principal::from_slice(&[u8::MAX; 29]);
pic.set_controllers(canister_id, None, vec![Principal::anonymous(), user_id])
.unwrap();

let controllers = pic.get_controllers(canister_id);
assert_eq!(controllers.len(), 2);
assert!(controllers.contains(&Principal::anonymous()));
assert!(controllers.contains(&user_id));
}

#[test]
#[should_panic(expected = "CanisterNotFound(CanisterId")]
fn get_controllers_of_nonexisting_canister() {
let pic = PocketIc::new();

let canister_id = pic.create_canister();
pic.add_cycles(canister_id, 100_000_000_000_000);
pic.stop_canister(canister_id, None).unwrap();
pic.delete_canister(canister_id, None).unwrap();

let _ = pic.get_controllers(canister_id);
}
1 change: 1 addition & 0 deletions rs/pocket_ic_server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
of the endpoint `/instances/` to create a new PocketIC instance.
- New endpoint `/instances/<instance_id>/_/topology` returning the topology of the PocketIC instance.
- New CLI option `--log-levels` to specify the log levels for PocketIC server logs (defaults to `pocket_ic_server=info,tower_http=info,axum::rejection=trace`).
- New endpoint `/instances/<instance_id/read/get_controllers` to get the controllers of a canister.

### Fixed
- Renamed `dfx_test_key1` tECDSA and tSchnorr keys to `dfx_test_key`.
Expand Down
24 changes: 24 additions & 0 deletions rs/pocket_ic_server/src/pocket_ic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2333,6 +2333,30 @@ impl Operation for GetStableMemory {
}
}

#[derive(Copy, Clone, Debug)]
pub struct GetControllers {
pub canister_id: CanisterId,
}

impl Operation for GetControllers {
fn compute(&self, pic: &mut PocketIc) -> OpOut {
let subnet = pic.try_route_canister(self.canister_id);
match subnet {
Some(subnet) => subnet
.get_controllers(self.canister_id)
.map(OpOut::Controllers)
.unwrap_or(OpOut::Error(PocketIcError::CanisterNotFound(
self.canister_id,
))),
None => OpOut::Error(PocketIcError::CanisterNotFound(self.canister_id)),
}
}

fn id(&self) -> OpId {
OpId(format!("get_controllers({})", self.canister_id))
}
}

#[derive(Clone, Debug)]
pub struct GetCyclesBalance {
pub canister_id: CanisterId,
Expand Down
54 changes: 49 additions & 5 deletions rs/pocket_ic_server/src/state_api/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
use super::state::{ApiState, OpOut, PocketIcError, StateLabel, UpdateReply};
use crate::pocket_ic::{
AddCycles, AwaitIngressMessage, CallRequest, CallRequestVersion, CanisterReadStateRequest,
DashboardRequest, ExecuteIngressMessage, GetCanisterHttp, GetCyclesBalance, GetStableMemory,
GetSubnet, GetTime, GetTopology, MockCanisterHttp, PubKey, Query, QueryRequest,
SetStableMemory, SetTime, StatusRequest, SubmitIngressMessage, SubnetReadStateRequest, Tick,
DashboardRequest, ExecuteIngressMessage, GetCanisterHttp, GetControllers, GetCyclesBalance,
GetStableMemory, GetSubnet, GetTime, GetTopology, MockCanisterHttp, PubKey, Query,
QueryRequest, SetStableMemory, SetTime, StatusRequest, SubmitIngressMessage,
SubnetReadStateRequest, Tick,
};
use crate::{async_trait, pocket_ic::PocketIc, BlobStore, InstanceId, OpId, Operation};
use aide::{
Expand Down Expand Up @@ -38,8 +39,8 @@ use pocket_ic::common::rest::{
self, ApiResponse, AutoProgressConfig, ExtendedSubnetConfigSet, HttpGatewayConfig,
HttpGatewayDetails, InstanceConfig, MockCanisterHttpResponse, RawAddCycles, RawCanisterCall,
RawCanisterHttpRequest, RawCanisterId, RawCanisterResult, RawCycles, RawMessageId,
RawMockCanisterHttpResponse, RawSetStableMemory, RawStableMemory, RawSubmitIngressResult,
RawSubnetId, RawTime, RawWasmResult, Topology,
RawMockCanisterHttpResponse, RawPrincipalId, RawSetStableMemory, RawStableMemory,
RawSubmitIngressResult, RawSubnetId, RawTime, RawWasmResult, Topology,
};
use pocket_ic::WasmResult;
use serde::Serialize;
Expand Down Expand Up @@ -75,6 +76,7 @@ where
.directory_route("/topology", get(handler_topology))
.directory_route("/get_time", get(handler_get_time))
.directory_route("/get_canister_http", get(handler_get_canister_http))
.directory_route("/get_controllers", post(handler_get_controllers))
.directory_route("/get_cycles", post(handler_get_cycles))
.directory_route("/get_stable_memory", post(handler_get_stable_memory))
.directory_route("/get_subnet", post(handler_get_subnet))
Expand Down Expand Up @@ -363,6 +365,19 @@ impl TryFrom<OpOut> for () {
}
}

impl TryFrom<OpOut> for Vec<RawPrincipalId> {
type Error = OpConversionError;
fn try_from(value: OpOut) -> Result<Self, Self::Error> {
match value {
OpOut::Controllers(controllers) => Ok(controllers
.into_iter()
.map(|principal_id| principal_id.0.into())
.collect()),
_ => Err(OpConversionError),
}
}
}

impl TryFrom<OpOut> for RawCycles {
type Error = OpConversionError;
fn try_from(value: OpOut) -> Result<Self, Self::Error> {
Expand Down Expand Up @@ -583,6 +598,28 @@ pub async fn handler_mock_canister_http(
(code, Json(response))
}

pub async fn handler_get_controllers(
State(AppState { api_state, .. }): State<AppState>,
Path(instance_id): Path<InstanceId>,
headers: HeaderMap,
extract::Json(raw_canister_id): extract::Json<RawCanisterId>,
) -> (StatusCode, Json<ApiResponse<Vec<RawPrincipalId>>>) {
let timeout = timeout_or_default(headers);
match CanisterId::try_from(raw_canister_id.canister_id) {
Ok(canister_id) => {
let get_op = GetControllers { canister_id };
let (code, response) = run_operation(api_state, instance_id, timeout, get_op).await;
(code, Json(response))
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiResponse::Error {
message: format!("{:?}", e),
}),
),
}
}

pub async fn handler_get_cycles(
State(AppState { api_state, .. }): State<AppState>,
Path(instance_id): Path<InstanceId>,
Expand Down Expand Up @@ -803,6 +840,13 @@ async fn op_out_to_response(op_out: OpOut) -> Response {
)),
)
.into_response(),
opout @ OpOut::Controllers(_) => (
StatusCode::OK,
Json(ApiResponse::Success(
Vec::<RawPrincipalId>::try_from(opout).unwrap(),
)),
)
.into_response(),
opout @ OpOut::Cycles(_) => (
StatusCode::OK,
Json(ApiResponse::Success(RawCycles::try_from(opout).unwrap())),
Expand Down
9 changes: 8 additions & 1 deletion rs/pocket_ic_server/src/state_api/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ use hyper::{Request, Response as HyperResponse};
use hyper_util::client::legacy::{connect::HttpConnector, Client};
use ic_http_endpoints_public::cors_layer;
use ic_http_gateway::{CanisterRequest, HttpGatewayClient, HttpGatewayRequestArgs};
use ic_types::{canister_http::CanisterHttpRequestId, CanisterId, SubnetId};
use ic_types::{canister_http::CanisterHttpRequestId, CanisterId, PrincipalId, SubnetId};
use itertools::Itertools;
use pocket_ic::common::rest::{
CanisterHttpRequest, HttpGatewayBackend, HttpGatewayConfig, HttpGatewayDetails,
HttpGatewayInfo, Topology,
Expand Down Expand Up @@ -206,6 +207,7 @@ pub enum OpOut {
Time(u64),
CanisterResult(Result<WasmResult, UserError>),
CanisterId(CanisterId),
Controllers(Vec<PrincipalId>),
Cycles(u128),
Bytes(Vec<u8>),
StableMemBytes(Vec<u8>),
Expand Down Expand Up @@ -252,6 +254,11 @@ impl std::fmt::Debug for OpOut {
OpOut::Time(x) => write!(f, "Time({})", x),
OpOut::Topology(t) => write!(f, "Topology({:?})", t),
OpOut::CanisterId(cid) => write!(f, "CanisterId({})", cid),
OpOut::Controllers(controllers) => write!(
f,
"Controllers({})",
controllers.iter().map(|c| c.to_string()).join(",")
),
OpOut::Cycles(x) => write!(f, "Cycles({})", x),
OpOut::CanisterResult(Ok(x)) => write!(f, "CanisterResult: Ok({:?})", x),
OpOut::CanisterResult(Err(x)) => write!(f, "CanisterResult: Err({})", x),
Expand Down
22 changes: 17 additions & 5 deletions rs/state_machine_tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2705,18 +2705,30 @@ impl StateMachine {
))
}

/// Returns the controllers of a canister or `None` if the canister does not exist.
pub fn get_controllers(&self, canister_id: CanisterId) -> Option<Vec<PrincipalId>> {
let state = self.state_manager.get_latest_state().take();
state
.canister_state(&canister_id)
.map(|s| s.controllers().iter().cloned().collect())
}

pub fn install_wasm_in_mode(
&self,
canister_id: CanisterId,
mode: CanisterInstallMode,
wasm: Vec<u8>,
payload: Vec<u8>,
) -> Result<(), UserError> {
let state = self.state_manager.get_latest_state().take();
let sender = state
.canister_state(&canister_id)
.and_then(|s| s.controllers().iter().next().cloned())
.unwrap_or_else(PrincipalId::new_anonymous);
let sender = self
.get_controllers(canister_id)
.map(|controllers| {
controllers
.into_iter()
.next()
.unwrap_or(PrincipalId::new_anonymous())
})
.unwrap_or(PrincipalId::new_anonymous());
self.execute_ingress_as(
sender,
ic00::IC_00,
Expand Down

0 comments on commit c31d549

Please sign in to comment.