Недавно я обнаружил, что можно указать функцию защиты, которая будет выполняться перед функциями обновления и запроса смарт-контрактов контейнеров, написанных на Rust, на Интернет-компьютере.

Вы можете сказать мне, что следующий пост является просто плагиатом документации Crate, но, поскольку я понял, что это возможно, только взглянув на репозиторий уведомления о транзакциях OpenChat, я подумал, что это стоило поста 😄.

Хотите начать создавать децентрализованные приложения, не изучая новый язык программирования? Ознакомьтесь с Juno, платформой Блокчейн как услуга с открытым исходным кодом, которая позволяет быстрее и проще, чем когда-либо прежде, создавать децентрализованные приложения только с внешним кодом! ⚡️🚀🤯

Оригинальный подход

Я начал свой путь с Rust, когда перенес свой существующий код Motoko — то есть, когда я обновил существующие смарт-контракты Papyrs.

Поскольку эти контейнеры предназначались для пользовательских данных, мне пришлось перенести и функции, требующие контроля доступа.

Для этого я реализовал сравнение принципалов — т.е. сопоставляю вызывающих функций с пользователями, которые сохранены в состоянии. Если они равны, методы могут быть выполнены, если нет, я выдаю ошибки и отклоняю вызов.

Не зная, как писать охранники, я в основном дублировал if повсюду в каждом вызове, который нужно было защитить.

use candid::Principal;
use ic_cdk::{caller, trap};
use ic_cdk_macros::{query, init};
use std::cell::RefCell;

#[derive(Default)]
pub struct State {
    pub user: Option<Principal>,
}

// This canister cannot be created without user

#[init]
fn init(user: Principal) {
    STATE.with(|state| {
        *state.borrow_mut() = State { user: Some(user) };
    });
}

// Mutable global state.
// See Roman Kashitsyn's post for more info:
// https://mmapped.blog/posts/01-effective-rust-canisters.html

thread_local! {
    static STATE: RefCell<State> = RefCell::default();
}

#[query]
fn greet(name: String) -> String {
    let user: Principal = STATE.with(|state| state.borrow().user).unwrap();

    // 🖖 Here I check if the caller matches the user who owns the canister

    if user != caller() {
        trap("User does not have the permission to say hello.");
    }

    format!("Hello, {}!", name)
}

Хотя он работает так, как я ожидал, вы можете легко себе представить, что дублирование одного и того же кода — особенно по мере роста количества функций — медленно увеличивает читабельность.

Это было до того, как я, к счастью, обнаружил функцию guard 💪.

Сторожить

Стандартные сторожевые функции — это функции, которые могут выполняться перед функцией обновления или запроса. Когда они возвращают ошибку, соответствующий метод не выполняется.

Поэтому я изменил свой код, чтобы воспользоваться им и избежать дублирования.

Я создал новый модуль guards.rs для выполнения точно такого же сопоставления с образцом, как тот, который я реализовал в приведенном выше фрагменте кода.

use candid::Principal;
use ic_cdk::caller;
use crate::STATE;

pub fn caller_is_user() -> Result<(), String> {
    let caller = caller();
    let user: Principal = STATE.with(|state| state.borrow().user).unwrap();

    if caller == user {
        Ok(())
    } else {
        Err("Caller is not the user of the canister.".to_string())
    }
}

После установки я «просто» заменил свои существующие псевдотесты объявлением гвардии.

// ️🖖1️⃣ declare the new module
mod guards;

use candid::Principal;
use ic_cdk::{caller, trap};
use ic_cdk_macros::{query, init};
use std::cell::RefCell;
// 🖖2️⃣ the function needs to be imported
use crate::guards::caller_is_user;

#[derive(Default)]
pub struct State {
    pub user: Option<Principal>,
}

// This canister cannot be created without user

#[init]
fn init(user: Principal) {
    STATE.with(|state| {
        *state.borrow_mut() = State { user: Some(user) };
    });
}

// Mutable global state.
// See Roman Kashitsyn's post for more info:
// https://mmapped.blog/posts/01-effective-rust-canisters.html

thread_local! {
    static STATE: RefCell<State> = RefCell::default();
}

// 🖖3️⃣ set the guard by its function's name
#[query(guard = "caller_is_user")]
fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}

И, в принципе, это уже 🥳.

Рецепт:

  1. Создать сторожевую функцию, которая возвращает ошибку, если условия не выполняются
  2. Импортируйте модуль
  3. Аннотировать функции, которые необходимо защитить​
  4. Весело 😁

Резюме

Не самый длинный мой пост, который я когда-либо писал, но я надеюсь, что он когда-нибудь будет полезен для кого-то, как это было для меня, чтобы открыть для себя эти маленькие хитрости.

В бесконечность и дальше
Дэвид​

Чтобы узнать больше о приключениях, подписывайтесь на меня в Твиттере 🖖