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

[EPROD-969] Inspect message check and docs #2

Merged
merged 14 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# canister-upgrader

## Introduction

The upgrader canister allows the creation of polls to approve upgrade of other canisters.
This is achieved by allowing registered voters to approve or reject upgrades identified by unique hashes.

## Poll types

Thee different types of polls can be created:
1. `ProjectHash`: a poll to approve a specific project hash
1. `AddPermission`: a poll to grant permissions to a Principal
1. `RemovePermission`: a poll to remove permissions from a Principal

For each new poll, the creator has to provide the following informations:
- `description`: The description of the poll,
- `poll_type`: The type of poll as discussed above,
- `start_timestamp_secs`: The timestamp in seconds of when the poll opens
- `end_timestamp_secs`: The timestamp in seconds of when the poll closes

## User Permissions

The access to the canister features is restricted by a set of permissions that allow selected Pricipals to operate on the canister.
The available permissions are:

- `Admin`: this permission grants admin rights to the principal. An admin can directy grant or remove permissions to other principals
- `CreateProject`: Allows calling the endpoints to create a project (e.g. evm, bridge, etc.)
- `CreatePoll`: Allows calling the endpoints to create a poll
- `VotePoll`: Allows calling the endpoints to vote in a poll

28 changes: 28 additions & 0 deletions src/did/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ impl Storable for ProjectData {
const BOUND: ic_stable_structures::Bound = ic_stable_structures::Bound::Unbounded;
}

/// Data required to create a poll.
#[derive(
Debug, Clone, CandidType, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize,
)]
pub struct PollCreateData {
/// The description of the poll.
pub description: String,
/// The type of poll.
pub poll_type: PollType,
/// The timestamp when the poll opens.
pub start_timestamp_secs: u64,
/// The timestamp when the poll closes.
pub end_timestamp_secs: u64,
}

/// Describes the type of poll.
#[derive(
Debug, Clone, CandidType, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize,
Expand All @@ -106,6 +121,19 @@ pub struct Poll {
pub end_timestamp_secs: u64,
}

impl From<PollCreateData> for Poll {
fn from(value: PollCreateData) -> Self {
Self {
description: value.description,
poll_type: value.poll_type,
no_voters: Vec::new(),
yes_voters: Vec::new(),
start_timestamp_secs: value.start_timestamp_secs,
end_timestamp_secs: value.end_timestamp_secs,
}
}
}

/// Describes the type of poll.
#[derive(
Debug, Clone, CandidType, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize,
Expand Down
1 change: 1 addition & 0 deletions src/upgrader_canister/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ic-log = { workspace = true }
ic-storage = { workspace = true }
ic-stable-structures = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
upgrader_canister_did = { workspace = true }

[dev-dependencies]
Expand Down
72 changes: 58 additions & 14 deletions src/upgrader_canister/src/canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ use std::collections::BTreeMap;
use candid::Principal;
use ic_canister::{init, post_upgrade, query, update, Canister, MethodType, PreUpdate};
use ic_exports::ic_kit::ic;
use ic_stable_structures::stable_structures::Memory;
use upgrader_canister_did::error::Result;
use upgrader_canister_did::{
BuildData, Permission, PermissionList, Poll, ProjectData, UpgraderCanisterInitData,
BuildData, Permission, PermissionList, Poll, PollCreateData, PollType, ProjectData,
UpgraderCanisterInitData, UpgraderError,
};

use crate::build_data::canister_build_data;
use crate::state::permission::Permissions;
use crate::state::UpgraderCanisterState;

thread_local! {
Expand Down Expand Up @@ -87,6 +90,22 @@ impl UpgraderCanister {
})
}

/// Disable/Enable the inspect message
#[update]
pub fn admin_disable_inspect_message(&mut self, value: bool) -> Result<()> {
STATE.with(|state| {
state.permissions.borrow().check_admin(&ic::caller())?;
state.settings.borrow_mut().disable_inspect_message(value);
Ok(())
})
}

/// Returns whether the inspect message is disabled.
#[query]
pub fn is_inspect_message_disabled(&self) -> bool {
STATE.with(|state| state.settings.borrow().is_inspect_message_disabled())
}

/// Returns the permissions of the caller
#[query]
pub fn caller_permissions_get(&self) -> Result<PermissionList> {
Expand All @@ -108,14 +127,19 @@ impl UpgraderCanister {
STATE.with(|state| state.projects.borrow().get(&key))
}

/// Inspects permissions for the project_create method
pub fn project_create_inspect<M: Memory>(
permissions: &Permissions<M>,
caller: &Principal,
) -> Result<()> {
permissions.check_has_all_permissions(caller, &[Permission::CreateProject])
}

/// Creates a new project
#[update]
pub fn project_create(&mut self, project: ProjectData) -> Result<()> {
STATE.with(|state| {
state
.permissions
.borrow()
.check_has_all_permissions(&ic::caller(), &[Permission::CreateProject])?;
Self::project_create_inspect(&state.permissions.borrow(), &ic::caller())?;
state.projects.borrow_mut().insert(project)
})
}
Expand All @@ -132,27 +156,47 @@ impl UpgraderCanister {
STATE.with(|state| state.polls.borrow().get(&id))
}

/// Inspects permissions for the poll_create method
pub fn poll_create_inspect<M: Memory>(
permissions: &Permissions<M>,
caller: &Principal,
) -> Result<()> {
permissions.check_has_all_permissions(caller, &[Permission::CreatePoll])
}

/// Creates a new poll and returns the generated poll id
#[update]
pub fn poll_create(&mut self, poll: Poll) -> Result<u64> {
pub fn poll_create(&mut self, poll: PollCreateData) -> Result<u64> {
STATE.with(|state| {
state
.permissions
.borrow()
.check_has_all_permissions(&ic::caller(), &[Permission::CreatePoll])?;
Self::poll_create_inspect(&state.permissions.borrow(), &ic::caller())?;

if let PollType::ProjectHash { project, hash: _ } = &poll.poll_type {
state.projects.borrow().get(project).ok_or_else(|| {
UpgraderError::BadRequest(format!(
"Cannot create poll, project [{}] does not exist",
project
))
})?;
}

Ok(state.polls.borrow_mut().insert(poll))
})
}

/// Inspects permissions for the poll_vote method
pub fn poll_vote_inspect<M: Memory>(
permissions: &Permissions<M>,
caller: &Principal,
) -> Result<()> {
permissions.check_has_all_permissions(caller, &[Permission::VotePoll])
}

/// Votes for a poll. If the voter has already voted, the previous vote is replaced.
#[update]
pub fn poll_vote(&mut self, poll_id: u64, approved: bool) -> Result<()> {
STATE.with(|state| {
let caller = ic::caller();
state
.permissions
.borrow()
.check_has_all_permissions(&caller, &[Permission::VotePoll])?;
Self::poll_vote_inspect(&state.permissions.borrow(), &caller)?;
state
.polls
.borrow_mut()
Expand Down
1 change: 1 addition & 0 deletions src/upgrader_canister/src/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub(crate) const PERMISSIONS_MAP_MEMORY_ID: u8 = 1;
pub(crate) const PROJECTS_MAP_MEMORY_ID: u8 = 2;
pub(crate) const POLLS_MAP_MEMORY_ID: u8 = 3;
pub(crate) const POLLS_ID_SEQUENCE_MEMORY_ID: u8 = 4;
pub(crate) const SETTINGS_MAP_MEMORY_ID: u8 = 5;
42 changes: 42 additions & 0 deletions src/upgrader_canister/src/inspect_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// required by the inspect_message macro
#[allow(unused_imports)]
use ic_exports::ic_cdk::{self, api};
use ic_exports::ic_kit::ic;

use crate::canister::UpgraderCanister;
use crate::state::UpgraderCanisterState;

/// NOTE: inspect is disabled for non-wasm targets because without it we are getting a weird compilation error
/// in CI:
/// > multiple definition of `canister_inspect_message'
#[cfg(target_family = "wasm")]
#[ic_exports::ic_cdk_macros::inspect_message]
fn inspect_messages() {
crate::canister::STATE.with(|state| inspect_message_impl(state))
}

#[allow(dead_code)]
fn inspect_message_impl(state: &UpgraderCanisterState) {
// If inspect message is disabled, accept the message
if state.settings.borrow().is_inspect_message_disabled() {
api::call::accept_message();
return;
}

let permissions = state.permissions.borrow();
let method = api::call::method_name();

let check_result = match method.as_str() {
method if method.starts_with("admin_") => permissions.check_admin(&ic::caller()),
"project_create" => UpgraderCanister::project_create_inspect(&permissions, &ic::caller()),
"poll_create" => UpgraderCanister::poll_create_inspect(&permissions, &ic::caller()),
"poll_vote" => UpgraderCanister::poll_vote_inspect(&permissions, &ic::caller()),
_ => Ok(()),
};

if let Err(e) = check_result {
ic::trap(&format!("Call rejected by inspect check: {e:?}"));
} else {
api::call::accept_message();
}
}
1 change: 1 addition & 0 deletions src/upgrader_canister/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use ic_canister::generate_idl;
pub mod build_data;
pub mod canister;
pub mod constant;
pub mod inspect_message;
pub mod state;

pub fn idl() -> String {
Expand Down
4 changes: 4 additions & 0 deletions src/upgrader_canister/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ use ic_stable_structures::stable_structures::DefaultMemoryImpl;
use ic_stable_structures::{default_ic_memory_manager, VirtualMemory};
use permission::Permissions;
use polls::Polls;
use settings::Settings;

pub mod permission;
pub mod polls;
pub mod projects;
pub mod settings;

/// State of the upgrader canister
pub struct UpgraderCanisterState {
pub permissions: Rc<RefCell<Permissions<VirtualMemory<DefaultMemoryImpl>>>>,
pub polls: Rc<RefCell<Polls<VirtualMemory<DefaultMemoryImpl>>>>,
pub projects: Rc<RefCell<projects::Projects<VirtualMemory<DefaultMemoryImpl>>>>,
pub settings: Rc<RefCell<Settings<VirtualMemory<DefaultMemoryImpl>>>>,
}

impl Default for UpgraderCanisterState {
Expand All @@ -25,6 +28,7 @@ impl Default for UpgraderCanisterState {
permissions: Rc::new(RefCell::new(Permissions::new(&memory_manager))),
polls: Rc::new(RefCell::new(Polls::new(&memory_manager))),
projects: Rc::new(RefCell::new(projects::Projects::new(&memory_manager))),
settings: Rc::new(RefCell::new(Settings::new(&memory_manager))),
}
}
}
30 changes: 9 additions & 21 deletions src/upgrader_canister/src/state/polls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use ic_stable_structures::{
BTreeMapStructure, CellStructure, MemoryManager, StableBTreeMap, StableCell,
};
use upgrader_canister_did::error::{Result, UpgraderError};
use upgrader_canister_did::Poll;
use upgrader_canister_did::{Poll, PollCreateData};

use crate::constant::{POLLS_ID_SEQUENCE_MEMORY_ID, POLLS_MAP_MEMORY_ID};

Expand Down Expand Up @@ -36,9 +36,9 @@ impl<M: Memory> Polls<M> {
}

/// Inserts a new poll and returns the generated key
pub fn insert(&mut self, poll: Poll) -> u64 {
pub fn insert(&mut self, poll: PollCreateData) -> u64 {
let id = self.next_id();
self.polls.insert(id, poll);
self.polls.insert(id, poll.into());
id
}

Expand Down Expand Up @@ -115,10 +115,8 @@ mod test {
let mut polls = super::Polls::new(&memory_manager);

// Act
let poll_0_id = polls.insert(upgrader_canister_did::Poll {
let poll_0_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
Expand All @@ -127,10 +125,8 @@ mod test {
end_timestamp_secs: 234567,
});

let poll_1_id = polls.insert(upgrader_canister_did::Poll {
let poll_1_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_1".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
Expand Down Expand Up @@ -165,10 +161,8 @@ mod test {
// Arrange
let memory_manager = ic_stable_structures::default_ic_memory_manager();
let mut polls = super::Polls::new(&memory_manager);
let poll_id = polls.insert(upgrader_canister_did::Poll {
let poll_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
Expand Down Expand Up @@ -202,10 +196,8 @@ mod test {
// Arrange
let memory_manager = ic_stable_structures::default_ic_memory_manager();
let mut polls = super::Polls::new(&memory_manager);
let poll_id = polls.insert(upgrader_canister_did::Poll {
let poll_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
Expand Down Expand Up @@ -247,10 +239,8 @@ mod test {

let end_ts = 100;

let poll_id = polls.insert(upgrader_canister_did::Poll {
let poll_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
Expand Down Expand Up @@ -278,10 +268,8 @@ mod test {

let start_ts = 100;

let poll_id = polls.insert(upgrader_canister_did::Poll {
let poll_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
Expand Down
Loading