LoginSignup
11
2

More than 1 year has passed since last update.

Internet Computerを用いたDApp開発とUXの評価

Last updated at Posted at 2022-04-25

はじめに

最近、Web3.0分散型Web)に対する注目度が高まっています。一般にブロックチェーンは非中央集権的な仕組みをもち、ブロックチェーンの活用によってWeb3.0の実現を目指すプロジェクトはいくつもあります。そのなかでも、総合的なサービス提供基盤としての地位を確立しようとしているのが Internet Computer というブロックチェーンです。

Internet Computerとは

分散コンピューティングプラットフォームの実現を目指しているコンソーシアム型ブロックチェーン1です。

Canisterというスマートコントラクトをもつのが特徴で、トランザクションの記録やコントラクトのデータ処理機能に加え、大容量データのストレージやアプリのホスティング環境を提供しています。

Internet Computerは、スイスに拠点を置くDFINITY Foundationが開発を推進しています。2021年5月にブロックチェーンがオープンソースになりました。

参考:DFINITY Foundation | Internet Computer

これまでのブロックチェーンとの違い

分散型アプリ(DApp)の実行環境を提供するブロックチェーンはいくつもありますが、最も大きなエコシステムをもつのはEthereumブロックチェーンでしょう。

Ethereumはコントラクトアドレスを指定し、ブロックチェーン上に記録されたコントラクトのバイトコードをEVM2上で実行することでデータを書き換えます。

扱えるデータのサイズはMB単位くらいが限界です。もし、1GBのデータをEthereumブロックチェーンに記録しようとすると、理論上はガス代だけで100億米ドルを超える費用がかかります。また、2022年4月現在Ethereumブロックチェーンに1日で保存されるデータ量はおよそ570MBで、1GBのデータを書き込むには少なくとも2日間トランザクションを占有することになります3

そもそもEthereumブロックチェーンのフルノードのデータでも数百GBしかなく、ストレージのような利用方法は想定されていません。

また、Webアプリケーション本体を動かす環境は提供されていないため、DAppは外部のサーバーでホストします。

これに対し、Internet ComputerではCanister IDを指定し、Canister内部に保存されたソースコードを実行します。一つのCansiterには8GBまでデータのストレージ領域が用意されているため、大容量データも扱うことができます。

ソースコードには従来のようなトランザクションの処理、ブロックチェーン上に記録されたデータの書き換えだけでなく、Webアプリケーション本体を載せてホストすることも可能です。

Canisterの仕組み

Canisterはデータ処理領域とストレージ領域に分けられます。

  • データ処理領域:WebAssembly(Wasm)を格納
  • ストレージ領域:ブロックチェーンに保存したい内容を記録

データ処理の際には、Ethereumのスマートコントラクトと同様に、トークンを消費することでコンピューティングリソースを利用します。Internet ComputerはネイティブトークンであるICPトークンをもち、Cycleというトークンに変換してCanisterにデポジットすることで手数料の支払いが可能です(Ethereumでいうガス代に相当)。

Ethereumのスマートコントラクトを利用する際にはトランザクション手数料の支払いが必要なため、Metamaskなどの暗号資産ウォレットが必要でした。しかし、CanisterはあらかじめCycleをデポジットしているため、暗号資産ウォレットを必要としません。

ICPトークンはユーティリティトークンで、ノード運用報酬やガバナンス報酬の支払いにも用いられます。

UXに関する技術検証

検証概要

Internet Computerを利用した場合、従来のWebサービス提供環境と比べてどの程度快適に利用できるのかについて検証を行いました。

今回は、簡易的な家計簿アプリを作成し、以下の二つの環境にそれぞれデプロイしました。

  • Internet Computer
  • Microsoft Azure Virtual Machine

アプリの実装内容や開発方法は後ほど「実装内容」の項目にて説明します。

評価方法

ユーザーエクスペリエンスを評価するための検証項目として、大きく三つの項目を設定しました。

  • サーバー応答時間
  • Initial connection:TCPやSSL/TLSの接続を確立するための初期接続にかかる時間
  • TTFB(Time To First Byte):HTTPリクエストが送信されてからデータを受信するまでの待機時間
  • ページ描画時間(サーバー応答時間含む):CanisterやWebサーバーに接続してからページリソースやデータを取得し、DOMを描画するまでにかかる時間
  • データ処理時間:データの作成/参照/更新/削除にかかる時間

評価指標の作成にあたり、以下を参考にしました。

それぞれの検証項目は、Apache JMeterやChrome DevToolsを用いて計測しました(利用しているブラウザはGoogle Chromeです)。

検証結果

Internet ComputerにデプロイしたCanisterとAzure VMを利用した場合で、各検証項目の時間を計測した結果を以下の表に示しています。

Internet Computer Azure VM
接続確立
Initial connection
500ms 128ms
データ取得待機
TTFB
957ms 264ms
ページ描画
(データ読み込み無)
3,196ms 1,478ms
ページ描画
(データ読み込み有)
4,811ms 2,094ms
データ作成 4,124ms 260ms
データ読み込み 1,193ms 250ms
データ更新 4,186ms 253ms
データ削除 4,232ms 326ms

※検証項目それぞれについて10回実測した値の平均を示しています。

Azure VMで動作させた場合と比べると、Webページを表示させるのにかかる時間はおよそ2.2倍、データ処理にかかる時間はおよそ4.8〜16.5倍になりました。

Googleの調査4によると、読み込みに3秒以上かかるモバイルサイトはリンクを開いたユーザーのうち53%が離脱することがわかっています。Canisterからのデータ読み込みを伴うページ描画には5秒近くかかっていることから、実際に運用していくためにはもう少し改善が必要と考えられます。

また、Internet Computerはノード間のコンセンサスを求めるデータ作成/更新/削除に関してはおよそ2秒、データの変更を伴わずコンセンサスがいらないデータ読み込みに関しては数十ミリ秒で実行するという目標を立てています。

検証結果では前者で4秒以上、後者で1秒以上の時間がかかっており、目標に対してまだ差が大きい状態です。

検証から得られた技術的所見

Internet Computerのページ描画やデータ処理に時間がかかる理由には、以下の二つが考えられます。

TTFBが長い

TTFBの長さはWebサイトに初めてアクセスするときだけでなく、ページ遷移やデータ処理のためのリクエスト送信など、さまざまなタイミングに影響を及ぼします。

TTFBが長くなる原因はいくつか考えられます。

  • DNS Lookupに時間がかかる
  • インフラのリソースが不足している
  • ネットワークの通信速度が遅い

Internet Computerのノードは欧米を中心に配置されているため、日本から接続すると遅延が大きくなると予想されます。しかし、今回利用したAzure VMも米西海岸のインスタンスを利用しているため、地理的な要因だけで遅延が発生しているとは考えにくいでしょう。検証結果に生じた差はドメインの解決やリソースの不足が原因であると考えるのが自然です。

2022年4月現在のInternet Computerのノード数は443台ですが、デプロイされているCanisterの数はおよそ5.7万に達しています5。また、WebアプリからCanisterへ送信されたHTTPSリクエストの処理やルーティングなどは10台のBoundary Nodesが担っています。

Canisterの数と比べるとどちらも十分とはいえませんが、Internet Computerの描く未来ではノードを数百万台規模にまで増やすことを想定しています。資金面などから容易に増やせるものではありませんが、今後運用されるノードの数が増えていけばリソース不足による遅延の改善が見込まれます。

Canister間の通信に時間がかかる

Canisterにはそれぞれ異なるドメイン(FQDN)が割り当てられています。現在の仕様ではCanister IDにひもづいて決定されているため、カスタムドメインは利用できません。

一方、Webブラウザの仕様では異なるドメイン間の接続にはCORS(Cross Origin Resource Sharing)という仕組みを利用することになります。CORSを用いた通信ではリクエストの送信前に通信先の確認(Preflight request)を必要とし、これにより通信時間が長くなっています。

開発者がドメインを自由に割り当てられるように、Internet Computer向けのDNSが開発される計画はありますが、具体的な予定は示されていません。また、複数のCanisterに全く同じドメインを割り当て可能かは不明のため、CORSによる遅延の解消は短期的には難しいと考えられます。

同じドメインを使用している場合は同一生成元ポリシー(Same Origin Policy)が適用されるため、Preflight requestは発生しません。

Azure VM環境は同一のインスタンスに全てのサービスを展開しているため、Preflight requestが含まれない場合の結果を示しています。

実装内容

最後に、今回検証に用いたWebアプリの実装内容について簡単に説明します。

サービスの構成

Internet Computer

Internet Computerにおけるアプリケーションの構成

Canisterはフロントエンド/バックエンド両方の機能を提供できますが、互いのファイルは分割する必要があります。

Canisterは実行時の役割に合わせてタイプを指定することができます。フロントエンドとしてWebアプリをホストする場合には assets を指定し、バックエンド処理を行う場合には motoko などの言語を指定します。

Web3層モデルを例に考えると、フロントエンドはプレゼンテーション層に相当し、バックエンドはアプリケーション層とデータベース層に相当します。Canisterごと機能を分割して実装できるため、バックエンドの処理をさらに細かく分けることも可能です。

今回作成するサービスでは、フロントエンドを担うCanisterでWebアプリをホストし、バックエンドではCanisterのストレージ領域に保存したデータをCRUDする機能を実装しています。

Microsoft Azure

Azure VMにおけるアプリケーションの構成

AzureのVirtual Machine(Standard D2s 3: vCPU 2, Memory 8GiB)を利用します。今回は一つのサーバーに全ての機能をもたせる構成です。

Webアプリケーションを提供するため、以下のようにサービスを構築します。

  • Webサーバー機能:Nginx
  • APIサーバー機能:Express(Node.js)
  • データベース機能:MySQL

フロントエンドはReact.jsで作成し、ビルドしたStaticコンテンツをNginxでホストします。フロントエンドからデータベースを利用するため、Expressを用いてREST APIを提供しています。

ソースコード

今回作成したのは、簡単な家計簿アプリです。CanisterまたはMySQLに収支として日付、カテゴリ、内容、金額を記録します。

Internet Computer

フロントエンドはReact.jsが利用できます。Canisterはバンドル後のindex.jsを読み込むため、Webpackを利用してビルドするのであれば必ずしもReact.jsを利用する必要はありません。

バックエンドにはInternet Computerの専用言語として開発されたMotokoを利用しています。Wasmはさまざまな言語のコンパイルターゲットにでき、汎用言語のRustなども利用できます。

Canisterのタイプはdfx.jsonで指定します。

dfx.json
{
  "canisters": {
    "demo_app": {
      "main": "src/demo_app/main.mo",
      "type": "motoko"
    },
    "demo_app_assets": {
      "dependencies": [
        "demo_app"
      ],
      "frontend": {
        "entrypoint": "src/demo_app_assets/src/index.html"
      },
      "source": [
        "src/demo_app_assets/assets",
        "dist/demo_app_assets/"
      ],
      "type": "assets"
    }
  },
  ...
}

フロントエンド

ここでは、家計簿に記録した収支アイテムの一つを表示・編集するページを例にとって説明します。

edit.jsx
import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';

// ビルドするとCanisterを利用するためのインターフェースが自動で生成される
import { demo_app } from '../../declarations/demo_app';

const Edit = () => {
  let [searchParams, setSearchParams] = useSearchParams();
  const [id, setId] = useState(0);
  const [date, setDate] = useState('');
  const [category, setCategory] = useState('');
  const [content, setContent] = useState('');
  const [amount, setAmount] = useState(0);

  async function updateItem() {
    const body = {
      id,
      date,
      category,
      content,
      amount: Number(amount),
    };

    // 読み込んだインターフェースを用いてCanisterと通信する
    // 非同期処理のため、Processを用いて実装する
    demo_app.update(id, body).then(() => {
      location.href = '/';
    });
  }

  async function deleteItem() {
    const body = {
      id,
      date,
      category,
      content,
      amount: Number(amount),
    };

    // 収支アイテムのIDと内容を引数として、一致するアイテムを削除する
    demo_app.delete(id, body).then(() => {
      location.href = '/';
    });
  }

  async function fetchItem() {
    const getItem = async () => {
      // アイテムのIDを引数として収支アイテムを取得する
      return demo_app.getItem(itemId).then((items) => {
        return items[0];
      });
    };

    const param = searchParams.get('id');
    const itemId = Number(param);
    setId(itemId);

    const item = await getItem();
    setDate(item.date);
    setCategory(item.category);
    setContent(item.content);
    setAmount(Number(item.amount));
  }

  // ページをロードしたとき、収支アイテムを取得する
  useEffect(async () => {
    fetchItem();
  }, []);

  return (
    <div>
      <div className="field">
        <label className="label">日付</label>
        <div className="control">
          <input
            className="input"
            value={date}
            onChange={(ev) => setDate(ev.target.value)}
            placeholder="YYYY/MM/DD"
          ></input>
        </div>
      </div>
      <div className="field">
        <label className="label">カテゴリ</label>
        <div className="control">
          <input
            className="input"
            value={category}
            onChange={(ev) => setCategory(ev.target.value)}
          ></input>
        </div>
      </div>
      <div className="field">
        <label className="label">内容</label>
        <div className="control">
          <input
            className="input"
            value={content}
            onChange={(ev) => setContent(ev.target.value)}
          ></input>
        </div>
        <p className="help">30字以内</p>
      </div>
      <div className="field">
        <label className="label">金額</label>
        <div className="control">
          <input
            className="input"
            value={amount}
            onChange={(ev) => setAmount(ev.target.value)}
          ></input>
        </div>
      </div>
      <div className="is-flex mt-5">
        <button
          className="button is-primary is-light mr-2"
          onClick={updateItem}
        >
          更新
        </button>
        <button className="button is-danger is-light" onClick={deleteItem}>
          削除
        </button>
      </div>
    </div>
  );
};

export default Edit;

上記のように、Canisterとの通信部分はPromiseを用いて自動生成されたインターフェースを呼び出すことで簡単に実装できます。

fetch関数やAxiosを用いてREST APIを呼び出すように実装できるため、従来のWeb技術の多くを活用できることがわかります。

Ethereumではweb3.jsというパッケージを用いて同じように実装できますが、暗号資産ウォレットと接続するなどの処理が必要になります。一方、Canisterは暗号資産ウォレット不要のため、より従来のWebアプリに近い実装になります。

バックエンド

Motokoは独自言語で、Solidityのように開発用パッケージが一通り揃っているわけではありません。

DFINITYはVS Code向けにSyntax Highlightを提供していますが、IntelliSenceやAuto formatterは提供されていません。快適に実装するにはこれらの機能も整備されることが望ましいでしょう(Qiitaでは未対応の言語のため、下記のソースコードもハイライトが効いていません)。

main.mo
// Motoko基本パッケージの読み込み
import L "mo:base/List";
import A "mo:base/AssocList";
import Types "./types";

actor DemoApp {

  type NewItem = Types.NewItem;
  type Item = Types.Item;
  type ItemId = Types.ItemId;

  // Canisterが更新された場合にも保持される変数
  stable var newItemId: Nat = 1;

  // 収支アイテムとIDをひもづける
  flexible var accountBook: A.AssocList<ItemId, Item> = L.nil<(ItemId, Item)>();

  func isEq(l: ItemId, r: ItemId): Bool {
      return l == r;
  };

  // 収支アイテムを新しく作成、保存する
  public func new(newItem: NewItem) : async () {
    let item = {
      id = newItemId;
      date = newItem.date;
      category = newItem.category;
      content = newItem.content;
      amount = newItem.amount;
    };

    let (newAccountBook, _) = A.replace<ItemId, Item>(accountBook, newItemId, isEq, ?item);
    accountBook := newAccountBook;
    newItemId += 1;
  };

  // 作成済みの収支アイテムの内容を書き換える
  public func update(itemId: ItemId, item: Item) : async () {
    let (newAccountBook, _) = A.replace<ItemId, Item>(accountBook, itemId, isEq, ?item);
    accountBook := newAccountBook;
  };
  
  // 収支アイテムを削除する
  // ※AssocListには削除機能がないため、今回はdiff関数を用いて自作
  public func delete(itemId: ItemId, item: Item) : async () {
    let tmpAccountBook: A.AssocList<ItemId, Item> = L.nil<(ItemId, Item)>();
    let (newAccountBook, _) = A.replace<ItemId, Item>(tmpAccountBook, itemId, isEq, ?item);
    accountBook := A.diff<ItemId, Item, Item>(accountBook, newAccountBook, isEq);
  };

  // 収支アイテムを取得する
  // データの取得のみを行う場合はコンセンサス不要のため、queryを付与する
  public query func getItem(itemId: ItemId) : async ?Item {
    return A.find<ItemId, Item>(accountBook, itemId, isEq);
  };

  public query func length() : async Nat {
    newItemId - 1;
  };
};

上記のうち、データの書き換えが必要、すなわちコンセンサスが必要な処理はUpdate callと呼びます。反対に、データの取得のみを行い、データを書き換えない処理はQuery callと呼びます。

参考:https://github.com/4ita/ic_demo

Microsoft Azure

Internet Computerとできるだけ条件を合わせるため、フロントエンドはReact.jsで実装しています。

バックエンドはNode.jsを用いたサーバーを構築できるExpressというパッケージで、MySQLと接続するためのREST APIを作成しています。

詳細は下記のリポジトリをご参照ください。

参考:(Webアプリ)https://github.com/4ita/react_demo, (API)https://github.com/4ita/db_api

SDKの利用

DFINITY Canister SDKは次のような機能をもっています。

  • Canisterのビルド、デプロイ
  • デプロイ済みCanisterの操作
  • Canisterとの通信
  • Canisterのローカルテスト環境の提供

Canister SDKはNode.js環境で動作します。

コマンド利用例を以下に示します。

terminal
// Install DFINITY Canister SDK
$ sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"

// Create a sample canister from the template
$ dfx new <project's name>
// Install required packages
$ npm i

// Run the local node 
$ dfx start
$ dfx stop

// Genetate Canister ID
$ dfx canister create --all
// Build a canister from the source
$ dfx build
// Deploy a canister on the test environment
$ dfx canister install --all

// Interact with the canister
// Update call
$ dfx canister call <canister name> <function> <arguments>
("result")
// Query call
$ dfx canister call --query <canister name> <function>
("result")

--all フラグを利用することで、作成したフロントエンド / バックエンドのCanisterを一度に処理できます。Canister名を指定すれば個別実行も可能です。

また、デプロイのワークフローは下記のコマンドで一気に行うこともできます。

terminal
$ dfx deploy

まとめ

Canisterは既存のスマートコントラクトと比べて、ストレージやWebアプリのホストなど総合的にWebサービスを提供するための機能をもっています。利用する際に暗号資産ウォレットも必要ないため、ユーザーにとっては利用しやすい環境になっています。

ただ、実際にアプリケーションを使ってみるとページ表示やデータの処理に従来の倍以上時間がかかるため、快適とはいえない状態です。長期的にInternet Computerを維持するノードが増えれば動作環境も改善される可能性はあります。しかし、ノードを増やすのは容易ではないため、短期的に現在のクラウドを置き換えるような環境が提供できる見込みは薄いと考えられます。

冒頭でも触れたように、最近はWeb3.0への注目が高まっています。従来のブロックチェーンにおいて取引データなどはノードに分散化されていますが、アプリケーションの実行基盤はクラウドなど外部のサービスを利用しているのが現状です。一方で、Internet Computerはその実行基盤まで分散することを目指したブロックチェーンのため、Web3.0の理想により近いブロックチェーンとして注目したいプロジェクトの一つと感じています。

参考

検証用のWebアプリは以下のフレームワークやソースコードを参考に作成しました。

※図中で使用したロゴについて、React.js ©️ reactjs.org クリエイティブ・コモンズ・ライセンス(表示4.0 国際)

  1. DFINITYをはじめ、複数のVCやブロックチェーン企業が加盟しているInternet Computer Associationがノード管理者を承認・管理しています。

  2. Ethereum Virtual Machineと呼ばれるスマートコントラクトの実行環境

  3. 2022年4月19日時点での平均ブロック生成時間は13.29秒、平均ブロックサイズは92,499Bytes (参考:https://etherscan.io/

  4. 参考:Find Out How You Stack Up to New Industry Benchmarks for Mobile Page Speed | Think with Google

  5. 出典:Internet Computer Network Status(閲覧日:2022年4月19日)

11
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
2