Skip to content

Commit

Permalink
feat: [IC-272] add fetch_canister_logs simple query call stub
Browse files Browse the repository at this point in the history
  • Loading branch information
maksymar committed Jan 30, 2024
1 parent 5e94025 commit 939a5e6
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 10 deletions.
65 changes: 58 additions & 7 deletions rs/execution_environment/src/query_handler.rs
Expand Up @@ -13,6 +13,7 @@ use crate::{
hypervisor::Hypervisor,
metrics::{MeasurementScope, QueryHandlerMetrics},
};
use candid::Encode;
use ic_btc_interface::NetworkInRequest as BitcoinNetwork;
use ic_config::execution_environment::Config;
use ic_config::flag_status::FlagStatus;
Expand All @@ -36,7 +37,7 @@ use ic_types::{
Blob, Certificate, CertificateDelegation, HttpQueryResponse, HttpQueryResponseReply,
UserQuery,
},
CanisterId, NumInstructions,
CanisterId, NumInstructions, PrincipalId,
};
use serde::Serialize;
use std::convert::Infallible;
Expand All @@ -51,7 +52,10 @@ use tokio::sync::oneshot;
use tower::{util::BoxCloneService, Service};

pub(crate) use self::query_scheduler::{QueryScheduler, QuerySchedulerFlag};
use ic_ic00_types::{BitcoinGetBalanceArgs, BitcoinGetUtxosArgs, Payload, QueryMethod};
use ic_ic00_types::{
BitcoinGetBalanceArgs, BitcoinGetUtxosArgs, FetchCanisterLogsRequest,
FetchCanisterLogsResponse, LogVisibility, Payload, QueryMethod,
};
use ic_replicated_state::NetworkTopology;

/// Convert an object into CBOR binary.
Expand Down Expand Up @@ -209,18 +213,32 @@ impl QueryHandler for InternalHttpQueryHandler {
if query.receiver == CanisterId::ic_00() {
let network = match QueryMethod::from_str(query.method_name.as_str()) {
Ok(QueryMethod::BitcoinGetUtxosQuery) => {
let args = BitcoinGetUtxosArgs::decode(&query.method_payload)?;
args.network
BitcoinGetUtxosArgs::decode(&query.method_payload)?.network
}
Ok(QueryMethod::BitcoinGetBalanceQuery) => {
let args = BitcoinGetBalanceArgs::decode(&query.method_payload)?;
args.network
BitcoinGetBalanceArgs::decode(&query.method_payload)?.network
}
Ok(QueryMethod::FetchCanisterLogs) => {
return match self.config.fetch_canister_logs {
FlagStatus::Enabled => fetch_canister_logs(
query.source.get(),
state.get_ref(),
FetchCanisterLogsRequest::decode(&query.method_payload)?,
),
FlagStatus::Disabled => Err(UserError::new(
ErrorCode::CanisterContractViolation,
format!(
"{} API is not enabled on this subnet",
QueryMethod::FetchCanisterLogs
),
)),
}
}
Err(_) => {
return Err(UserError::new(
ErrorCode::CanisterMethodNotFound,
format!("Query method {} not found.", query.method_name),
))
));
}
};

Expand Down Expand Up @@ -293,6 +311,39 @@ impl QueryHandler for InternalHttpQueryHandler {
}
}

fn fetch_canister_logs(
sender: PrincipalId,
state: &ReplicatedState,
args: FetchCanisterLogsRequest,
) -> Result<WasmResult, UserError> {
let canister_id = args.get_canister_id();
let canister = state.canister_state(&canister_id).ok_or_else(|| {
UserError::new(
ErrorCode::CanisterNotFound,
format!("Canister {canister_id} not found"),
)
})?;

match canister.log_visibility() {
LogVisibility::Public => Ok(()),
LogVisibility::Controllers if canister.controllers().contains(&sender) => Ok(()),
LogVisibility::Controllers => Err(UserError::new(
ErrorCode::CanisterRejectedMessage,
format!(
"Caller {} is not allowed to query ic00 method {}",
sender,
QueryMethod::FetchCanisterLogs
),
)),
}?;

// TODO(IC-272): temporarily return empty logs until full implementation is ready.
let response = FetchCanisterLogsResponse {
canister_log_records: Vec::new(),
};
Ok(WasmResult::Reply(Encode!(&response).unwrap()))
}

impl HttpQueryHandler {
pub(crate) fn new_service(
internal: Arc<dyn QueryHandler<State = ReplicatedState>>,
Expand Down
152 changes: 149 additions & 3 deletions rs/execution_environment/tests/fetch_canister_logs.rs
Expand Up @@ -2,14 +2,15 @@ use ic_config::execution_environment::Config as ExecutionConfig;
use ic_config::flag_status::FlagStatus;
use ic_config::subnet_config::SubnetConfig;
use ic_ic00_types::{
CanisterInstallMode, CanisterSettingsArgsBuilder, FetchCanisterLogsRequest, LogVisibility,
Payload,
CanisterInstallMode, CanisterSettingsArgsBuilder, FetchCanisterLogsRequest,
FetchCanisterLogsResponse, LogVisibility, Payload,
};
use ic_registry_subnet_type::SubnetType;
use ic_state_machine_tests::{
ErrorCode, PrincipalId, StateMachine, StateMachineBuilder, StateMachineConfig, UserError,
};
use ic_test_utilities::universal_canister::UNIVERSAL_CANISTER_WASM;
use ic_test_utilities_execution_environment::get_reply;
use ic_types::{CanisterId, Cycles};

fn setup(fetch_canister_logs: FlagStatus) -> (StateMachine, CanisterId) {
Expand Down Expand Up @@ -112,4 +113,149 @@ fn test_fetch_canister_logs_ingress_enabled() {
);
}

// TODO(IC-272): add query call tests.
#[test]
fn test_fetch_canister_logs_query_disabled() {
// Arrange.
// - disable the fetch_canister_logs API
// - set the log visibility to public so that any user can read the logs
let (env, canister_id) = setup(FlagStatus::Disabled);
let not_a_controller = PrincipalId::new_user_test_id(42);
env.update_settings(
&canister_id,
CanisterSettingsArgsBuilder::new()
.with_log_visibility(LogVisibility::Public)
.build(),
)
.unwrap();
// Act.
// Make a query call.
let result = env.query_as(
not_a_controller,
CanisterId::ic_00(),
"fetch_canister_logs",
FetchCanisterLogsRequest {
canister_id: canister_id.into(),
}
.encode(),
);
// Assert.
// Expect to get an error because the fetch_canister_logs API is disabled,
// despite the fact that the log visibility is set to public.
assert_eq!(
result,
Err(UserError::new(
ErrorCode::CanisterContractViolation,
"fetch_canister_logs API is not enabled on this subnet"
))
);
}

#[test]
fn test_fetch_canister_logs_query_log_visibility_public_succeeds() {
// Arrange.
// - enable the fetch_canister_logs API
// - set the log visibility to public so that any user can read the logs
let (env, canister_id) = setup(FlagStatus::Enabled);
let not_a_controller = PrincipalId::new_user_test_id(42);
env.update_settings(
&canister_id,
CanisterSettingsArgsBuilder::new()
.with_log_visibility(LogVisibility::Public)
.build(),
)
.unwrap();
// Act.
// Make a query call.
let result = env.query_as(
not_a_controller,
CanisterId::ic_00(),
"fetch_canister_logs",
FetchCanisterLogsRequest {
canister_id: canister_id.into(),
}
.encode(),
);
// Assert.
// Expect some non-empty result.
assert_eq!(
FetchCanisterLogsResponse::decode(&get_reply(result)).unwrap(),
FetchCanisterLogsResponse {
canister_log_records: vec![]
}
);
}

#[test]
fn test_fetch_canister_logs_query_log_visibility_invalid_controller_fails() {
// Arrange.
// - enable the fetch_canister_logs API
// - restrict log visibility to controllers only
let (env, canister_id) = setup(FlagStatus::Enabled);
let not_a_controller = PrincipalId::new_user_test_id(42);
env.update_settings(
&canister_id,
CanisterSettingsArgsBuilder::new()
.with_log_visibility(LogVisibility::Controllers)
.build(),
)
.unwrap();
// Act.
// Make a query call from a non-controller.
let result = env.query_as(
not_a_controller,
CanisterId::ic_00(),
"fetch_canister_logs",
FetchCanisterLogsRequest {
canister_id: canister_id.into(),
}
.encode(),
);
// Assert.
// Expect an error because the caller is not a controller.
assert_eq!(
result,
Err(UserError::new(
ErrorCode::CanisterRejectedMessage,
format!(
"Caller {not_a_controller} is not allowed to query ic00 method fetch_canister_logs"
),
))
);
}

#[test]
fn test_fetch_canister_logs_query_log_visibility_valid_controller_succeeds() {
// Arrange.
// - enable the fetch_canister_logs API
// - restrict log visibility to controllers only
// - add new controller
let (env, canister_id) = setup(FlagStatus::Enabled);
let new_controller = PrincipalId::new_user_test_id(42);
env.update_settings(
&canister_id,
CanisterSettingsArgsBuilder::new()
.with_log_visibility(LogVisibility::Controllers)
.with_controller(new_controller)
.build(),
)
.unwrap();
// Act.
// Make a query call from a controller.
let result = env.query_as(
new_controller,
CanisterId::ic_00(),
"fetch_canister_logs",
FetchCanisterLogsRequest {
canister_id: canister_id.into(),
}
.encode(),
);
// Assert.
// Expect some non-empty result.
assert_eq!(
FetchCanisterLogsResponse::decode(&get_reply(result)).unwrap(),
FetchCanisterLogsResponse {
canister_log_records: vec![]
}
);
}
32 changes: 32 additions & 0 deletions rs/types/ic00_types/src/lib.rs
Expand Up @@ -2246,6 +2246,7 @@ impl Payload<'_> for BitcoinSendTransactionInternalArgs {}
pub enum QueryMethod {
BitcoinGetUtxosQuery,
BitcoinGetBalanceQuery,
FetchCanisterLogs,
}

/// `CandidType` for `NodeMetricsHistoryArgs`
Expand Down Expand Up @@ -2314,6 +2315,37 @@ impl FetchCanisterLogsRequest {
}
}

/// `CandidType` for `CanisterLogRecord`
/// ```text
/// record {
/// idx: nat;
/// timestamp_nanos: nat;
/// content: blob;
/// }
/// ```
#[derive(Default, Clone, CandidType, Deserialize, Debug, PartialEq)]
pub struct CanisterLogRecord {
pub idx: u64,
pub timestamp_nanos: u64,
#[serde(with = "serde_bytes")]
pub content: Vec<u8>,
}

impl Payload<'_> for CanisterLogRecord {}

/// `CandidType` for `FetchCanisterLogsResponse`
/// ```text
/// record {
/// canister_log_records: vec canister_log_record;
/// }
/// ```
#[derive(Default, Clone, CandidType, Deserialize, Debug, PartialEq)]
pub struct FetchCanisterLogsResponse {
pub canister_log_records: Vec<CanisterLogRecord>,
}

impl Payload<'_> for FetchCanisterLogsResponse {}

/// Struct used for encoding/decoding
/// `(record {
/// canister_id: principal;
Expand Down

0 comments on commit 939a5e6

Please sign in to comment.