【Rust/Blockchain】Internet Computer におけるスマートコントラクトを構築する
はじめに
今回は Internet Computer というブロックチェーンにおけるスマートコントラクトの開発を紹介します。
Internet Computer とはこれまでの IT を必要とせずに全てブロックチェーンのみから実行される新しい Web3 サービスやアプリケーションをホスティングできるというコンセプトのNon-EVM なブロックチェーンです。
Source: https://twitter.com/MessariCrypto/status/1391757884661411842
開発者がこの Internet Computer にコントラクトをデプロイするには定められた仕様にしたがった WASM モジュールを生成する必要がありますが、Internet Computer は様々な言語用のライブラリを公開しており、開発のために多くの言語の選択肢があります。
今回はこの Internet Computer のコントラクトを Rust で開発してみます。TODO リストの管理を行うアプリケーションを実際に実装するハンズオンを行なってみましょう。
そもそも Internet Computer とは?
歴史
Internet Computer は2016年10月に設立された DFINITY foundation によって主導され、2021年5月に誕生したブロックチェーンです。創設者である Dominic は初期の Ethereum コミュニティに参加し、暗号理論やコンセンサスに関する研究や開発をしていた。革新的だったそれらの技術 (BLS閾値暗号など) は当時の Ethereum コミュニティを説得して実装するのは困難だったため、独立し、最終的に Internet Computer を立ち上げてその技術が組み込まれています。
特徴
簡単に Internet Computer の特徴について触れておきます。Internet Computer はこれまでのブロックチェーンのスマートコントラクトを発展させた "canister software" を使って、ほぼ全てのシステムやサービスを分散型ネットワーク上に構築することを可能にしています。
1. Web experience
Internet Computer は Web2 との体験の差が生まれないようにするための工夫があります。Internet Computer のキャニスタースマートコントラクトは HTTP リクエストを処理することができる。これによりほとんどのオンラインサービスを Internet Computer 上のブロックチェーンから実行可能になる。またブロックチェーン外のアクセスを可能にし、コストが高く低速な Oracle を使用せずに直接 Web からデータを取得することができる。直接 Web と通信することにおける懸念点は Web の不安定な応答だが、Internet Computer は "HTTPS outcalls" という機能により、Web からの応答に関する合意形成を可能にし、信頼度を向上させています。
References
- https://internetcomputer.org/https-outcalls
- https://internetcomputer.org/docs/current/developer-docs/integrations/https-outcalls/https-outcalls-how-it-works
2. Canister Smart Contract by Web Assembly
ブロックチェーンにおけるスマートコントラクトはチューリング完全である必要があり、トークンの使用や取引が可能である必要があります。Internet Computer はこのスマートコントラクトを "Canister" という概念で実現しています。この Canister は後述する署名メカニズムが組み込まれていたり、他のブロックチェーンのスマートコントラクトよりも安価な保存領域としてのメモリを組み込んでいたりといくつか特徴がありますが、特に開発者が最も大きく意識しやすいプログラム部分について、そのロジックは WebAssembly モジュールによって提供されます。
Web Assembly (WASM) は汎用コードを実行するための仮想マシンです。当初 Web の世界で、Web 上でネイティブに近いパフォーマンスで実行する、かつ安全な実行環境として利用を想定されていました。WASM は安全かつ高速かつ決定論的であることを意識して設計された仮想マシンであるため、Internet Computer のプログラムモジュールとして採用されている。また WASM を利用する大きなもう一つの利点は、様々な言語によってサポートされていることです。このことにより、Internet Computer の開発者は自分が既に成熟した言語を選択して独自のロジックを組むことができます。
Source: https://medium.com/dfinity/webassembly-on-the-internet-computer-a1d0c71c5b94
References
3. Cross-chain transaction signing
"Threshold ECDSA" と "chain-key cryptography" との組み合わせによって提供される Internet Computer 特有の署名メカニズムは、それぞれの Bridge を必要とせずに他のブロックチェーンと直接やりとりを行うことを可能にしている。これによって Ethereum などのトランザクションを直接作成することができます。
Source: https://internetcomputer.org/docs/current/developer-docs/integrations/t-ecdsa/t-ecdsa-how-it-works
References
- https://internetcomputer.org/how-it-works/chain-key-technology/
- https://internetcomputer.org/docs/current/developer-docs/integrations/t-ecdsa/t-ecdsa-how-it-works
4. Web3 auth using WebAuthn
Ethereum など一般的なブロックチェーンはたった一つの秘密鍵によって、そのユーザーの全ての操作権限やウォレットそのものをコントロールしており、この鍵の管理を中心に認証機能やそこにまつわるセキュリティは長く存在している課題です。Internet Computer は W3C の Web 認証標準に基づいた認証機能である "Internet Identity" を提供しています。この Internet Identity が Internet Computer 上の Dapps の認証に利用されるネイティブな ID 形式となっています。
References
5. Reverse Gas Model
ユーザーにおけるブロックチェーンに関する大きなハードルの一つに Gas 管理がある。一般的に Gas とはブロックチェーンを利用する際に発生する手数料だが、ユーザーが操作をリクエストするたびに要求される。Internet Computer ではスマートコントラクト開発者がその燃料をキャニスター自体に補充しておくという設計がされており、利用者は都度料金を支払う必要がない。
References
他にも Bitcoin や Ethereum とのネイティブなインテグレーションなどもあります。もっと気になる方は是非下記リンクなどをみてみてください。
References
ハンズオン
今回は Internet Computer の Canister の WASM モジュールを Rust を利用して構築していきます。Rust の場合は、Canister を構築するために ic-cdk クレートを利用します。
Reference: Github - https://github.com/dfinity/cdk-rs
今回実装するコードは以下のリポジトリで公開しています。必要に応じて参考にしてください。
事前準備
ローカル開発環境を構築するために Internet Computer を操作するための CLI である dfx
を利用する必要があります。
プロジェクト生成
dfx new
を利用して、テンプレートのキャニスター実装環境のプロジェクトを生成します。以下のコマンドを実行してください。
% dfx new --type rust --no-frontend ic_todolist
Creating new project "ic_todolist"...
CREATE ic_todolist/dfx.json (179B)...
CREATE ic_todolist/.gitignore (248B)...
CREATE ic_todolist/README.md (2.47KiB)...
CREATE ic_todolist/src/ic_todolist_backend/src/lib.rs (86B)...
CREATE ic_todolist/src/ic_todolist_backend/ic_todolist_backend.did (51B)...
CREATE ic_todolist/src/ic_todolist_backend/Cargo.toml (334B)...
CREATE ic_todolist/Cargo.toml (57B)...
CREATE ic_todolist/src/ic_todolist_frontend/assets/sample-asset.txt (24B)...
Creating git repository...
warning: virtual workspace defaulting to `resolver = "1"` despite one or more workspace members being on edition 2021 which implies `resolver = "2"`
note: to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest
note: to use the edition 2021 resolver, specify `workspace.resolver = "2"` in the workspace root's manifest
note: for more details see https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions
Updating crates.io index
===============================================================================
Welcome to the internet computer developer community!
You're using dfx 0.15.1
...
To learn more before you start coding, see the documentation available online:
- Quick Start: https://internetcomputer.org/docs/current/tutorials/deploy_sample_app
- SDK Developer Tools: https://internetcomputer.org/docs/current/developer-docs/setup/install/
- Motoko Language Guide: https://internetcomputer.org/docs/current/motoko/main/about-this-guide
- Motoko Quick Reference: https://internetcomputer.org/docs/current/motoko/main/language-manual
- Rust CDK Guide: https://internetcomputer.org/docs/current/developer-docs/backend/rust/
If you want to work on programs right away, try the following commands to get started:
cd ic_todolist
dfx help
dfx new --help
===============================================================================
すると、以下のような Rust プロジェクトが生成されています。
% cd ic_todolist
% ls -R .
Cargo.lock Cargo.toml README.md dfx.json src
./src:
ic_todolist_backend ic_todolist_frontend
./src/ic_todolist_backend:
Cargo.toml ic_todolist_backend.did src
./src/ic_todolist_backend/src:
lib.rs
./src/ic_todolist_frontend:
assets
./src/ic_todolist_frontend/assets:
sample-asset.txt
今回フロントエンド部分は不要なので、フロントエンドに関するリソースは削除しましょう。下記のようにファイルの削除と更新を行なってください。
% git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: dfx.json
deleted: src/ic_todolist_frontend/assets/sample-asset.txt
dfx.json
はこのプロジェクトに属するキャニスターモジュールを管理するための設定ファイルです。こちらから ic_todolist_frontend
について削除してください。
{
"canisters": {
"ic_todolist_backend": {
"candid": "src/ic_todolist_backend/ic_todolist_backend.did",
"package": "ic_todolist_backend",
"type": "rust"
- },
- "ic_todolist_frontend": {
- "dependencies": [
- "ic_todolist_backend"
- ],
- "source": [
- "src/ic_todolist_frontend/assets"
- ],
- "type": "assets"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}
ここまででバックエンド機能を持った1つのキャニスターを管理するためのプロジェクトが作成できました。次のセクションから実際のコーディングに入っていきます。
コーディング
事前準備/前提知識
早速コーディングを始めていきたいのですが、先に今回使用するライブラリを紹介します。
-
candid
: for interface description language- Internet Computer におけるインターフェースフォーマットである
Candid
を Rust で扱うためのクレートです。- Canister は様々な言語から実装可能なので、それらのインタフェースを共通化します
- また定義されている型には Rust との互換性があり (Internet Computer 特有の言語の Motoko を除き) Rust が最も親和性が高いです
- Repository: dfinity/candid: Candid Library for the Internet Computer
- Internet Computer におけるインターフェースフォーマットである
-
ic-cdk
: Canister Developer Kit for the Internet Computer- Rust におけるキャニスターの作成/管理を可能にする
- WASM が実装すべきキャニスターインタフェースの実装を容易にし、システムAPI の呼び出しなどもこのライブラリでサポートします
- Repository: dfinity/cdk-rs: Rust canister development kit for the Internet Computer.
テンプレートで生成されるプロジェクトのライブラリは少し古いため、まずライブラリの更新から行いましょう。
-candid = "0.8"
-ic-cdk = "0.7"
-ic-cdk-timers = "0.1" # Feel free to remove this dependency if you don't need timers
+candid = "^0.10"
+ic-cdk = "^0.12"
現在のバージョンを確認したい場合は ic-cdk
のバージョンと、その依存関係を確認して candid
のバージョンを定めてください。
ic-cdk versions| crates.io: Rust Package Registry
本題のキャニスターコードの着手をしましょう。テンプレートで生成されるコードは以下のようになっています。
#[ic_cdk::query]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
少しこのコードについて解説を入れておきます。
#[ic_cdk::query]
を付与することで、その関数を外部呼び出し可能なキャニスターのインタフェースとして公開します。キャニスターのエンドポイントは query
と update
の2種類のみあります。これは Solidity における view function かどうかの違いと同様のものという理解で問題ないと思います。キャニスターのステータスを更新する処理は update
で、そうでないものは query
で呼び出すことができます。
Non-committing query calls (any state change is discarded).
Committing update calls (state changes are persisted).
Source: https://internetcomputer.org/docs/current/concepts/canisters-code#query-and-update-methods
この query function として公開された greet
は引数に String をとり、レスポンスの型が String となります。
ここでこのキャニスターのインタフェースである .did
ファイルを確認しましょう。
service : {
"greet": (text) -> (text) query;
}
この .did
ファイルには先ほど紹介した Candid フォーマットによるキャニスターのインタフェース情報が記録されています。現在の内容はテンプレート生成時に同時に生成されたものです。先ほどの greet
関数は Candid フォーマットに従うと "greet": (text) -> (text) query
と記されます。Candid 自体の仕様については以下を参照ください。今回のハンズオンで必要な部分については随時紹介していきます。
要件の確認
改めて要件の確認をしてみましょう。今回はすごく単純なTODOリストを実現するキャニスターを実装してみます。
最小限のデータ構造と、参照/新規作成/ステータス更新のアクションが可能なエンドポイントを構築してみます。
以下のようなインタフェースのイメージです。
struct Todo { ... }
#[ic_cdk::query]
fn get(idx: u64) -> Todo { ... }
#[ic_cdk::update]
fn add(contents: String) -> u64 { ... } // return ID
#[ic_cdk::update]
fn update_status(idx: u64, is_completed: bool) { ... }
モックの実装
早速実装を進めていきます。このキャニスターはTODOリスト自体を管理しなくてはいけないため、キャニスターのメモリを保存領域として使用する必要があります。
最初からそれを考慮すると難しくなるため、まずはダミーロジックでモックの作成をしてみましょう。TODO 自体を表すデータは以下のようにしましょう。
struct Todo {
contents: String,
is_completed: bool,
}
その上で各関数は以下のように実装してみます。
-
get
-> ダミーの TODO を返す Query function -
add
-> ダミーの Index である u64 を返す Update function -
update_status
-> 何もしない Update function
これらを実装するとコード全体は現在以下のようになります。
struct Todo {
contents: String,
is_completed: bool,
}
#[ic_cdk::query]
fn get(_idx: u64) -> Todo {
Todo { contents: "Dummy".to_string(), is_completed: false }
}
#[ic_cdk::update]
fn add(_contents: String) -> u64 {
0
}
#[ic_cdk::update]
fn update_status(_idx: u64, _is_completed: bool) {
// dummy
}
しかしこのままのコードだと一箇所エラーが出ていることが確認できるでしょう。
% cargo check
Checking ic_todolist_backend v0.1.0 (/Users/linnefromice/repository/github.com/_linnefromice/linnefromice/ic_todolist/src/ic_todolist_backend)
error[E0277]: the trait bound `Todo: CandidType` is not satisfied
--> src/ic_todolist_backend/src/lib.rs:9:1
|
9 | #[ic_cdk::query]
| ^^^^^^^^^^^^^^^^ the trait `CandidType` is not implemented for `Todo`
Todo 用の構造体に対して CandidType
derive が設定できていない、というエラーが出ています。
任意の構造体を Internet Computer 内で扱えるようにするために各構造体はどこでも同じように解釈できるように共通の規格に沿った Serialize/Deserialize できる必要があります。
candid
クレートではこれをサポートするために、CandidType
derive を提供しています。実際にこの derive を Todo の構造体に付与してみましょう。
+ use candid::CandidType;
+ #[derive(CandidType)]
struct Todo {
contents: String,
is_completed: bool,
}
これを付与して cargo check
を行うとエラーがなくなっていることが確認できます。
次に先ほど紹介した .did
ファイルを更新して、キャニスターのインタフェースを更新します。
今回の場合は以下のようになります。使っている部分だけ簡単に解説します。
type Todo = record { contents : text; is_completed : bool };
service : {
add : (text) -> (nat64);
get : (nat64) -> (Todo) query;
update_status : (nat64, bool) -> ();
}
service
セクションで公開するエンドポイントを定義します。それぞれのエンドポイントは "func_name" : ("request_type") -> ("response_type");
というフォーマットで記述します。
service : {
add : (text) -> (nat64);
get : (nat64) -> (Todo) query;
update_status : (nat64, bool) -> ();
}
Query function の場合は最後に query
を付与する必要があります。また今回は独自に定義したカスタム構造体をレスポンスの型に使用しています。その場合は type
を利用して宣言する必要があります。
type Todo = record { contents : text; is_completed : bool };
そして今回使用した Rust の Primitive な型について、String
-> text
, u64
-> nat64
となります。より深く知りたい人は以下を見てみてください。
以上でモック実装の上でエンドポイント自体の作成が完了しました。
メモリの宣言
データを保存するために、キャニスター内のメモリを使用します。今回は Heap memory を利用して、TODO を保存します。
Source: https://wiki.internetcomputer.org/wiki/IC_Smart_Contract_Memory
Heap memory を利用したストレージの宣言は以下のように行います。
thread_local
マクロを利用してミュータブルな static 変数を宣言します。
// For numerical counters
thread_local! {
static COUNTER: RefCell<u64> = RefCell::new(0);
}
今回の TODO アプリは配列で保存したいので、Vec<Todo>
として宣言します。
+use std::cell::RefCell;
use candid::CandidType;
+thread_local! {
+ static TODOS: RefCell<Vec<Todo>> = RefCell::new(Vec::new());
+}
これによってストレージの宣言は完了したので、次セクションで各関数でこのストレージを利用するように実装修正します。
メモリの利用
まずは add
から修正しましょう。入力されたコンテンツから Todo を作成し、これをストレージの可変長配列にプッシュします。
返却するインデックスは 配列の長さ - 1 で計算して返却します。
#[ic_cdk::update]
fn add(contents: String) -> u64 {
let val = Todo { contents, is_completed: false };
TODOS.with(|todos| {
let mut todos = todos.borrow_mut();
todos.push(val);
todos.len() as u64 - 1
})
}
次に get
を修正します。これはシンプルで、指定されたインデックスにある Todo を返却するのみです。
Todo
に Clone
derive を追加した上で下記のように修正しましょう。
#[derive(Clone, CandidType)]
struct Todo { ... }
#[ic_cdk::query]
fn get(idx: u64) -> Todo {
TODOS.with(|todos| {
let todos = todos.borrow();
todos[idx as usize].clone()
})
}
最後に update_status
を修正しましょう。get
の実装に近いですが、今回はメモリの配列から取得した Todo データを更新可能な形で取得した上で、is_completed
の値を更新します。
#[ic_cdk::update]
fn update_status(idx: u64, is_completed: bool) {
TODOS.with(|todos| {
let mut todos = todos.borrow_mut();
todos[idx as usize].is_completed = is_completed;
});
}
これでロジックの修正も完了しました!ダミーロジックだった時との差分は以下のようになります。
-#[derive(CandidType)]
+#[derive(Clone, CandidType)]
struct Todo {
contents: String,
is_completed: bool,
}
#[ic_cdk::query]
-fn get(_idx: u64) -> Todo {
- Todo { contents: "Dummy".to_string(), is_completed: false }
+fn get(idx: u64) -> Todo {
+ TODOS.with(|todos| {
+ let todos = todos.borrow();
+ todos[idx as usize].clone()
+ })
}
#[ic_cdk::update]
-fn add(_contents: String) -> u64 {
- 0
+fn add(contents: String) -> u64 {
+ let val = Todo { contents, is_completed: false };
+ TODOS.with(|todos| {
+ let mut todos = todos.borrow_mut();
+ todos.push(val);
+ todos.len() as u64 - 1
+ })
}
#[ic_cdk::update]
-fn update_status(_is_completed: bool) {
- // dummy
+fn update_status(idx: u64, is_completed: bool) {
+ TODOS.with(|todos| {
+ let mut todos = todos.borrow_mut();
+ todos[idx as usize].is_completed = is_completed;
+ });
}
これでキャニスターのロジックは全て実装完了しました。簡単にですが Todo のための構造体を宣言し、保存領域に Heap memory を利用し、簡易的な CRUD を作成しエンドポイントとして公開できています。次のセクションで実際にビルドしてローカルのノードにデプロイをしてみましょう。
ビルド/デプロイ
このハンズオンの最初に dfx
のセットアップを行い、dfx new
でテンプレートプロジェクトの作成で利用しました。dfx
には他にもローカルノードの稼働や、キャニスターのデプロイなどができます。いくつかのコマンドを利用して実際に動作確認してみましょう。
まずはローカルノードを起動します。dfx start --background
を実行してみてください。バックグラウンドでローカルノードを起動します。下記のような出力が確認できれば実行完了です。
% dfx start --background
Running dfx start for version 0.15.1
Using the default definition for the 'local' shared network because /Users/linnefromice/.config/dfx/networks.json does not exist.
Initialized replica.
Dashboard: http://localhost:56226/_/dashboard
次にビルドしてデプロイをしてみましょう。以下のコマンドを実行してください。
簡単に各コマンドについて説明すると dfx canister create
で空のキャニスターを構築し、dfx build
でキャニスターコードをビルドしてモジュール生成し、dfx canister install
で作成済のキャニスターにモジュールをインストールします。
dfx canister create --all
dfx build
dfx canister install --all
# or `dfx deploy`
実際にこのコマンドを実行すると以下のような出力がされます。出力内容から前述のようなアクションがされていることがわかると思います。
% dfx canister create --all && dfx build && dfx canister install --all
Creating a wallet canister on the local network.
The wallet canister on the "local" network for user "prod-2" is "bnz7o-iuaaa-aaaaa-qaaaa-cai"
Creating canister ic_todolist_backend...
ic_todolist_backend canister created with canister id: bkyz2-fmaaa-aaaaa-qaaaq-cai
Building canisters...
...
Finished release [optimized] target(s) in 0.03s
Creating UI canister on the local network.
The UI canister on the "local" network is "bd3sg-teaaa-aaaaa-qaaba-cai"
Installing code for canister ic_todolist_backend, with canister ID bkyz2-fmaaa-aaaaa-qaaaq-cai
dfx canister status ic_todolist_backend
で実際にインストールされた WASM モジュールのハッシュやその他のステータスが確認できます。以下のような出力が確認できればデプロイまでできています。
% dfx canister status ic_todolist_backend
Canister status call result for ic_todolist_backend.
Status: Running
Controllers: xxxxx-... xxxxx-...
Memory allocation: 0
Compute allocation: 0
Freezing threshold: 2_592_000
Memory Size: Nat(1606372)
Balance: 3_092_186_877_663 Cycles
Reserved: 0 Cycles
Reserved Cycles Limit: 5_000_000_000_000 Cycles
Module hash: 0x101b94738a69834ae064ef4d9ee50a2d8383c53c5e7ab6fc47935cef241fc619
動作確認
最後に実際にキャニスターをコールしてみましょう。dfx canister call (canister_name)
を利用することでローカルノードのキャニスターの呼び出しをすることができます。
まずは add
を呼び出して todo を追加してみましょう。
% dfx canister call ic_todolist_backend add '("homework")'
(0 : nat64)
% dfx canister call ic_todolist_backend add '("housework")'
(1 : nat64)
% dfx canister call ic_todolist_backend add '("shopping")'
(2 : nat64)
次に作成したタスクを get
で確認してみましょう。
% dfx canister call ic_todolist_backend get '(0)'
(record { contents = "homework"; is_completed = false })
% dfx canister call ic_todolist_backend get '(1)'
(record { contents = "housework"; is_completed = false })
% dfx canister call ic_todolist_backend get '(2)'
(record { contents = "shopping"; is_completed = false })
問題なく指定したコンテンツで Todo が作成できています!最後に update_status
でステータス更新をして、再度 get
で確認してみましょう。
# update_status
% dfx canister call ic_todolist_backend update_status '(0, true)'
()
% dfx canister call ic_todolist_backend update_status '(2, true)'
()
# get
% dfx canister call ic_todolist_backend get '(0)'
(record { contents = "homework"; is_completed = true })
% dfx canister call ic_todolist_backend get '(1)'
(record { contents = "housework"; is_completed = false })
% dfx canister call ic_todolist_backend get '(2)'
(record { contents = "shopping"; is_completed = true })
これで全ての公開したエンドポイントについて、想定通りの挙動が確認できました!以上で動作確認は完了です。
稼働させているローカルノードは dfx stop
で停止することができます。
% dfx stop
Using the default definition for the 'local' shared network because /Users/linnefromice/.config/dfx/networks.json does not exist.
Stopping canister http adapter...
Stopped.
Stopping the replica...
Stopped.
Stopping icx-proxy...
Stopped.
以上でシンプルなキャニスターの構築を通して、ゼロからキャニスタープロジェクトの構築と、そのキャニスターのデプロイまで体験することができました!
終わりに
これまで Solidity による Ethereum における開発のみでなく、Non-EVM な Cosmos や Aptos における開発も紹介してきました。今回の Internet Computer においては Rust で開発しましたが、Rust の強力なエコシステム/開発環境によって、最も実装を進めやすいと実感しました。Internet Computer に限らずコントラクトモジュールに WASM を採用するブロックチェーンは増えていますが、このように一般的なプログラミング言語を利用可能にすることで開発者の参入障壁をとても下げられていると思います。是非体験してみてください。
また Internet Computer は Web とのインテグレーションが強力であり、他のブロックチェーンとは異なる体験ができると思います!ドキュメントだけでも良いので目を通したり、触ってみたりするとより学びがあるかもしれません!
参考
Discussion