Asset Canisterの仕様
はじめに
Internet Computer(IC)は、Blockchain技術を応用した分散型クラウドと言われており、AWS、Google Clooud、Azureなどの特定クラウドプロバイダーに依存することなく、Canisterと呼ばれるSmart Contractを介して、インターネット上でコンピューティングを実現する仕組みを提供しています。
Blockchain技術によって分散化されたPaaS環境と捉えるとわかりやすいかもしれません。
CanisterにはWebAssemblyとデータが格納されていて、Cycleという単位の計算リソース使用料を支払うことにより、誰でもCanisterを作成して様々なサービスを提供できます。
RustやTypeScript、専用言語であるMotokoなど複数の言語でBackendを作成することができるほか、Dfinity Foundationが開発しているAsset Canisterを使用して、WebアプリケーションのFrontendを作成することもできます。
私は現在、数百年後の未来に家族の大切なデータを遺していく方法を模索しており、その一つの方法としてCanisterが利用できるのではないかと考えております。
そこで、その実現性を検討するため、まずはAsset Canisterについて少し調査してみました。
引用元: https://internetcomputer.org/img/webassembly/webassembly-hero-image.webp
Asset Canisterの使用例
Asset CanisterはアップロードしたファイルをHTTPプロトコル経由で取得する仕組みを備えています。
Asset Canisterとは何かを知るには、実際に使用してみるのが分かりやすいでしょう。Local Canister実行環境、または、Playground環境にデプロイしてみましょう。
もっともシンプルなAsset Canisterの使用例として、Canisterの設定ファイルdfx.json
と、Canisterに配置するindex.html
の2つを用意します。
ディレクトリ構造は、dfx new
コマンドが出力する合わせて深くしていますが、好みに応じて変更することもできます。
1. プロジェクトの作成
icptest
├── dfx.json
└── src
└── icptest_frontend
└── dist
└── index.html
dfx.json
{
"canisters": {
"icptest_frontend": {
"source": [
"src/icptest_frontend/dist"
],
"type": "assets",
"workspace": "icptest_frontend"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}
src/icptest_frontend/dist/index.html
<!DOCTYPE html>
<html>
<head>
<title>icp test</title>
</head>
<body>
<h1>hello, world</h1>
</body>
</html>
2. Deploy
Local Canister実行環境にデプロイするには、以下のようにするとよいでしょう。
Asset Canisterへのファイル配置はdfx deploy
コマンドと深く結びついているようで、コマンドの実行契機でdfx.json
内のsource
項目に記載されたローカルディレクトリとの同期が行われています。
$ cd icptest
$ dfx start --background --clean
$ dfx deploy
Internet Computer上のPlaygroundにデプロイするには、--playground
オプションを指定します。
また、--identity
オプションを指定すれば、ローカル開発時に使用するIDENTITYとは別にすることができます。
$ dfx deploy --identity <IDENTITY> --playground
Asset Canisterの仕様
Asset Canister I/F
Asset Canisterの仕組みを理解するために、まずは公開されているI/Fを確認してみることにしましょう。
2024年8月時点の公式ドキュメントには、以下のような記載があります。
#[update]
async fn authorize(other: Principal)
#[update]
async fn grant_permission(arg: GrantPermissionArguments)
#[update]
async fn validate_grant_permission(arg: GrantPermissionArguments) -> Result<String, String>
#[update]
async fn deauthorize(other: Principal)
#[update]
async fn revoke_permission(arg: RevokePermissionArguments)
#[update]
async fn validate_revoke_permission(arg: RevokePermissionArguments) -> Result<String, String>
#[update]
fn list_authorized() -> Vec<Principal>
#[query(manual_reply = true)]
fn list_permitted(arg: ListPermittedArguments) -> ManualReply<Vec<Principal>>
#[update]
async fn take_ownership()
#[query]
fn retrieve(key: Key) -> RcBytes
#[update(guard = "can_commit")]
fn store(arg: StoreArg)
#[update(guard = "can_prepare")]
fn create_batch() -> CreateBatchResponse
#[update(guard = "can_prepare")]
fn create_chunk(arg: CreateChunkArg) -> CreateChunkResponse
#[update(guard = "can_commit")]
fn create_asset(arg: CreateAssetArguments)
#[update(guard = "can_commit")]
fn set_asset_content(arg: SetAssetContentArguments)
#[update(guard = "can_commit")]
fn unset_asset_content(arg: UnsetAssetContentArguments)
#[update(guard = "can_commit")]
fn delete_asset(arg: DeleteAssetArguments)
#[update(guard = "can_commit")]
fn clear()
#[update(guard = "can_commit")]
fn commit_batch(arg: CommitBatchArguments)
#[query]
fn get(arg: GetArg) -> EncodedAsset
#[query]
fn get_chunk(arg: GetChunkArg) -> GetChunkResponse
#[query]
fn list() -> Vec<AssetDetails>
#[query]
fn certified_tree() -> CertifiedTree
SDK v0.22.0のDIDは以下に格納されています。
Gitリポジトリ
以下のリポジトリが用意されており、サーバ側機能はic-certified-assets
で、クライアント側機能はic-asset
で実装されています。
Project | 概要 | 備考 |
---|---|---|
ic-asset | a library for manipulating assets in an asset canister. | |
ic-certified-assets | Certified Assets Library | Rust CanisterにCertified assets機能を提供 |
ic-frontend-canister | Asset Canister | ic-certified-assetsに依存 |
icx-asset | icx-assetコマンド | ic-assetに依存 |
ファイル一覧の取得
サーバにアップロードされているファイルの一覧は、list()
メソッドで取得できます。
以下に、Local Canister実行環境に配置したAsset Canisterに対して、Rustで書いたクライアントプログラムからアクセスする例を示します。
1. プロジェクト作成
$ cargo new client
$ cd client
$ cargo add ic-agent ic-utils candid serde
$ cargo add tokio --features "macros rt-multi-thread"
2. クライアントプログラム
use candid::CandidType;
use ic_agent::Agent;
use ic_utils::call::SyncCall;
use ic_agent::identity::Secp256k1Identity;
use ic_agent::export::Principal;
use serde::Deserialize;
use std::env;
use std::collections::HashMap;
// [list]
/// Return a list of all assets in the canister.
#[derive(CandidType, Debug)]
pub struct ListAssetsRequest {}
/// Information about a content encoding stored for an asset.
#[derive(CandidType, Debug, Deserialize)]
pub struct AssetEncodingDetails {
/// A content encoding, such as "gzip".
pub content_encoding: String,
/// By convention, the sha256 of the entire asset encoding. This is calculated
/// by the asset uploader. It is not generated or validated by the canister.
pub sha256: Option<Vec<u8>>,
}
/// Information about an asset stored in the canister.
#[derive(CandidType, Debug, Deserialize)]
pub struct AssetDetails {
/// The key identifies the asset.
pub key: String,
/// A list of the encodings stored for the asset.
pub encodings: Vec<AssetEncodingDetails>,
/// The MIME type of the asset.
pub content_type: String,
}
#[tokio::main]
async fn main() {
// Identity
let home = env::var("HOME").unwrap();
let pem_path = format!("{}/.config/dfx/identity/default/identity.pem", home);
let identity = Secp256k1Identity::from_pem_file(pem_path).unwrap();
// Agent
let url = "http://127.0.0.1:4943";
let is_mainnet = false;
let canister_id = Principal::from_text("bkyz2-fmaaa-aaaaa-qaaaq-cai").unwrap();
let agent = Agent::builder()
.with_url(url)
.with_identity(identity)
.build()
.unwrap();
if !is_mainnet {
agent.fetch_root_key().await.unwrap();
}
// Canister
let canister = ic_utils::Canister::builder()
.with_agent(&agent)
.with_canister_id(canister_id)
.build()
.unwrap();
// List
let (entries,): (Vec<AssetDetails>,) = canister
.query("list")
.with_arg(ListAssetsRequest {})
.build()
.call()
.await.unwrap();
let assets: HashMap<_, _> = entries.into_iter().map(|d| (d.key.clone(), d)).collect();
println!("list: {:?}", assets);
}
3. プログラム実行
$ cargo run
list: {"/index.html": AssetDetails { key: "/index.html", encodings: [AssetEncodingDetails { content_encoding: "gzip", sha256: Some([68, 9, 14, 69, 197, 69, 48, 208, 49, 180, 87, 26, 156, 230, 121, 221, 225, 238, 251, 133, 187, 187, 162, 30, 26, 240, 129, 197, 119, 141, 81, 86]) }, AssetEncodingDetails { content_encoding: "identity", sha256: Some([136, 218, 251, 36, 243, 79, 112, 49, 76, 91, 117, 151, 73, 62, 179, 179, 192, 77, 240, 58, 38, 80, 116, 166, 112, 166, 91, 122, 160, 162, 13, 184]) }], content_type: "text/html" }}
ファイルアップロード
Asset Canisterへのファイルアップロードは、1回のリクエストサイズが2MB程度の上限があるため、クライアント-サーバ間のやりとりは少々複雑です。
大きいサイズのファイル、複数ファイルに対応したバッチアップロードを実現するために、create_batch()
、create_chunk()
、commit_batch()
などいくつかのメソッドが用意されています。
ic-asset
クレートを利用すれば、細かい通信手順を意識することなくファイルをアップロードを行うことができるようです。以下のような実装すればよさそうですが、ic-asset
の最終アップデートが2年前と古く微妙な感じです…。
︙
// Files
let mut key_map: HashMap<String, PathBuf> = HashMap::new();
key_map.insert("/favicon.ico".to_string(), PathBuf::from("favicon.ico"));
// Upload
let timeout = Duration::from_secs(30);
ic_asset::upload(&canister, timeout, key_map).await.unwrap();
Node.js
Node.jsからAsset Canisterにアップロードする例は、以下のドキュメントにあるようです。
ファイルの格納先
Asset CanisterにアップロードされたファイルはHeap Memory上で管理されています。ディスクのような補助記憶装置に相当するStable Memoryには格納しません。
そのため、格納できるファイル容量は約1GBまでとなっています。
おわりに
Asset Canisterは、WebアプリケーションなどFrontendをデプロイする環境として十分に使えるものですが、分散型ファイルストレージという用途としては物足らなさを感じます。
Canisterを分散型ファイルストレージとして利用するには、クライアントとのI/Fをオープンな仕様に整理して使いやすくするとともに、データ格納先もStable Memoryとし、内部のデータ構造もOSのファイルシステムのようにしっかりとした仕組みが必要となります。
正直なところ、Google DriveやOneDrive、iCloudといった商用クラウドのオンラインストレージは、ローカルPCやスマートフォンとの自動同期が行えたり、ファイルの共有やアプリケーション連携等、とても便利で素晴らしいものです。
数百年後の未来に大切なデータを遺していくという目的のために、『特定ベンダーに依存せずにオープンな仕様でデータを管理していくこと』がそもそも最適解なのかという点や、Internet ComputerのCanisterを分散型ファイルストレージとして扱えるようにする場合、どのように設計していけば良いかについて、今後、考えていければと思います。
Discussion