History
Overview
The Internet Computer tracks the history of a deployed canister through the canister's Wasm module hashes and controller changes. This tracking provides insight into the canister's history, such as what code has been used to run the canister and which controller deployed that code.
Viewing a canister's history can be important in use cases such as:
Verifying that a canister's controller has not maliciously modified the code.
Verifying that a canister's controller does not maliciously change the list of controllers for the canister. For example, if a canister's controller list only displays the governance canister as the current controller, users can trust that the canister's code has not been maliciously altered.
How canister history works
Canister history was introduced and released by the NNS DAO proposal 122617.
Canister history information is made available via inter-canister calls made to the canister_info
method of the management canister, using a canister's ID as an argument for the call. A canister's history stores the 20 most recent changes to the canister, including the canister's creation, Wasm module installations, reinstallations, upgrades, and changes to the list of canister controllers. It is important to note that since only the 20 most recent changes are stored, this feature cannot be used for auditing canisters that are frequently changed within a short period of time.
Canister history is only available for changes that have happened since this feature was rolled out on 023-06-05, 9:47:46 AM UTC, across all subnets.
You can read more about this feature on the developer forum.
Getting a canister's history
To get a canister's history, make a call to the IC management canister's canister_info
method:
- Motoko
- Rust
- TypeScript
- Python
import Principal "mo:base/Principal";
actor Info {
type canister_info_args = {
canister_id : Principal;
num_requested_changes : ?Nat64
};
type canister_info_result = {
total_num_changes : Nat64;
module_hash : ?Blob;
controllers : [Principal]
};
let IC = actor "aaaaa-aa" : actor {
canister_info : {canister_id : Principal} -> async canister_info_result
};
public func getInfo() : async canister_info_result {
await IC.canister_info {canister_id = Principal.fromActor(Info)}
}
}
use candid::{CandidType, Principal};
use ic_cdk::api::management_canister::main::{
canister_info, CanisterChange,
CanisterChangeDetails::{CodeDeployment, CodeUninstall, ControllersChange, Creation},
CanisterChangeOrigin::{FromCanister, FromUser},
CanisterInfoRequest, CanisterInfoResponse,
};
use serde::Deserialize;
/// Returns canister info with all available canister changes for a canister characterized by a given principal.
/// Traps if the canister_info management call is rejected (in particular, if the principal does not characterize a canister).
#[ic_cdk::update]
async fn info(canister_id: Principal) -> CanisterInfoResponse {
let request = CanisterInfoRequest {
canister_id,
num_requested_changes: Some(20),
};
canister_info(request).await.unwrap().0
}
/// Returns all (reflexive and transitive) controllers of a canister characterized by a given principal
/// by implementing BFS over the controllers.
#[ic_cdk::update]
async fn reflexive_transitive_controllers(canister_id: Principal) -> Vec<Principal> {
let mut ctrls = vec![canister_id];
let mut queue = vec![canister_id];
while !queue.is_empty() {
let cur = queue.pop().unwrap();
// check if the principal characterizes a canister by determining if it is an opaque principal
if cur.as_slice().last() == Some(&0x01) {
let info = info(cur).await;
for c in info.controllers {
if !ctrls.contains(&c) {
ctrls.push(c);
queue.push(c);
}
}
}
}
ctrls
}
/// Specifies a canister snapshot by providing a Unix timestamp in nanoseconds
/// or a canister version.
#[derive(CandidType, Deserialize, Clone)]
pub enum CanisterSnapshot {
#[serde(rename = "at_timestamp")]
AtTimestamp(u64),
#[serde(rename = "at_version")]
AtVersion(u64),
}
/// Maps the latest canister change of the canister characterized by a given principal before or at the given `CanisterSnapshot`
/// and returns an optional integer characterizing the maximum clock skew (in nanoseconds)
/// between the subnet hosting the canister and the given `CanisterSnapshot`
/// (i.e., if this integer is `None`, then no assumptions on clock skew are needed).
/// Returns `None` if no change to map can be determined due to unavailability of canister changes in canister history
/// or due to ambiguity between canister changes with the same timestamp.
/// Traps if a canister_info call is rejected (in particular, if the given principal does not characterize a canister).
async fn map_canister_change<T>(
canister_id: Principal,
canister_deployment: CanisterSnapshot,
f: impl Fn(&CanisterChange) -> Option<T>,
) -> Option<(T, Option<u64>)> {
let info = info(canister_id).await;
let mut map_change = None;
let mut skew = None;
for c in info.recent_changes {
if let Some(x) = f(&c) {
match &canister_deployment {
CanisterSnapshot::AtTimestamp(t) => {
if *t == c.timestamp_nanos {
return None;
}
if *t >= c.timestamp_nanos {
map_change = Some(x);
}
let d = if *t >= c.timestamp_nanos {
*t - c.timestamp_nanos
} else {
c.timestamp_nanos - *t
};
skew = Some(std::cmp::min(d, skew.unwrap_or(d)));
}
CanisterSnapshot::AtVersion(v) => {
if *v >= c.canister_version {
map_change = Some(x);
} else {
break;
}
}
};
}
}
map_change.map(|x| (x, skew))
}
/// Type for the result of `canister_controllers` calls.
#[derive(CandidType, Deserialize, Clone)]
struct CanisterControllersResult {
controllers: Vec<Principal>,
max_clock_skew: Option<u64>,
}
/// Returns the controllers of the canister characterized by a given principal and at the given `CanisterSnapshot`
/// and an optional integer characterizing the maximum clock skew (in nanoseconds)
/// between the subnet hosting the canister and the given `CanisterSnapshot`
/// (i.e., if this integer is `None`, then no assumptions on clock skew are needed).
/// Returns `None` if the controllers cannot be determined due to unavailability of canister changes in canister history
/// or due to ambiguity between canister changes with the same timestamp.
/// Traps if a canister_info call is rejected (in particular, if the given principal does not characterize a canister).
#[ic_cdk::update]
async fn canister_controllers(
canister_id: Principal,
canister_deployment: CanisterSnapshot,
) -> Option<CanisterControllersResult> {
map_canister_change(canister_id, canister_deployment, |c| match &c.details {
Creation(creation) => Some(creation.controllers.clone()),
ControllersChange(ctrls) => Some(ctrls.controllers.clone()),
_ => None,
})
.await
.map(|(controllers, max_clock_skew)| CanisterControllersResult {
controllers,
max_clock_skew,
})
}
/// Type for the result of `canister_module_hash` calls.
#[derive(CandidType, Deserialize, Clone)]
struct CanisterModuleHashResult {
module_hash: Option<Vec<u8>>,
max_clock_skew: Option<u64>,
}
/// Returns the module hash of the canister characterized by a given principal and at the given `CanisterSnapshot`
/// and an optional integer characterizing the maximum clock skew (in nanoseconds)
/// between the subnet hosting the canister and the given `CanisterSnapshot`
/// (i.e., if this integer is `None`, then no assumptions on clock skew are needed).
/// Returns `None` if the module hash cannot be determined due to unavailability of canister changes in canister history
/// or due to ambiguity between canister changes with the same timestamp.
/// Traps if a canister_info call is rejected (in particular, if the given principal does not characterize a canister).
#[ic_cdk::update]
async fn canister_module_hash(
canister_id: Principal,
canister_deployment: CanisterSnapshot,
) -> Option<CanisterModuleHashResult> {
map_canister_change(canister_id, canister_deployment, |c| match &c.details {
Creation(_) => Some(None),
CodeUninstall => Some(None),
CodeDeployment(code_deployment) => Some(Some(code_deployment.module_hash.clone())),
_ => None,
})
.await
.map(|(module_hash, max_clock_skew)| CanisterModuleHashResult {
module_hash,
max_clock_skew,
})
}
/// Type for the result of `canister_deployment_chain` calls.
#[derive(CandidType, Deserialize, Clone)]
struct CanisterDeploymentChainResult {
deployment_chain: Vec<CanisterChange>,
max_clock_skew: Option<u64>,
}
/// Returns the deployment chain of the canister characterized by a given principal and at the given `CanisterSnapshot`:
/// a list of canister changes starting with the change resulting in the canister deployment characterized by the given principal and at the given `CanisterSnapshot`
/// and with each subsequent change resulting in the canister deployment triggering the previous change,
/// and an optional integer characterizing the maximum clock skew (in nanoseconds)
/// between the subnets hosting the canisters from the deployment chain and the given `CanisterSnapshot`
/// (i.e., if this integer is `None`, then no assumptions on clock skew are needed).
/// The deployment chain stops if a canister change in the deployment chain cannot be determined (due to unavailability of canister changes in canister history
/// or due to ambiguity between canister changes with the same timestamp)
/// or upon encountering a change from a user principal or upon encountering a loop among canisters from the deployment chain.
/// Traps if a canister_info call is rejected (in particular, if the given principal does not characterize a canister).
#[ic_cdk::update]
async fn canister_deployment_chain(
canister_id: Principal,
canister_deployment: CanisterSnapshot,
) -> CanisterDeploymentChainResult {
let mut current_canister_id = canister_id;
let mut current_canister_deployment = canister_deployment;
let mut visited_canister_ids = vec![]; // canister IDs of canisters from the deployment chain
let mut deployment_chain = vec![];
let mut max_clock_skew = None;
loop {
visited_canister_ids.push(current_canister_id);
match map_canister_change(
current_canister_id,
current_canister_deployment.clone(),
|c| match &c.details {
CodeDeployment(_) => Some(c.clone()),
_ => None,
},
)
.await
{
Some((c, s)) => {
let mut done = false;
match &c.origin {
FromUser(_) => {
done = true;
}
FromCanister(o) => {
if visited_canister_ids.contains(&o.canister_id) {
done = true;
} else {
current_canister_id = o.canister_id;
current_canister_deployment = match o.canister_version {
None => CanisterSnapshot::AtTimestamp(c.timestamp_nanos),
Some(v) => CanisterSnapshot::AtVersion(v),
};
}
}
};
deployment_chain.push(c);
max_clock_skew = s
.map(|dt| Some(std::cmp::min(dt, max_clock_skew.unwrap_or(dt))))
.unwrap_or(max_clock_skew);
if done {
break;
}
}
None => {
break;
}
};
}
CanisterDeploymentChainResult {
deployment_chain,
max_clock_skew,
}
}
#[test]
fn check_candid_file() {
use candid_parser::utils::{service_equal, CandidSource};
let did_path = std::path::PathBuf::from(
std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR env var undefined"),
)
.join("canister-info.did");
candid::export_service!();
let expected = __export_service();
service_equal(
CandidSource::Text(&expected),
CandidSource::File(did_path.as_path()),
)
.unwrap();
}