MUD v2 Study(ETHGlobal Autonomous world)
ETHGlobalから、Hackasonの通知メールが届いてタイトルに惹かれて参加することにしました。
2023/5/18-5/24
ハッカソンではMUDという オンチェーンゲームのフレームワークを使うことが条件になっていましたのでMUD(v2:preview) のことを一通りStudyをしていこうとおもいます。
Overview 要約
MUDフレームワークはEVM(Ethereum Virtual Machine)アプリの構築を単純化するための強力なソフトウェアスタックです。MUDには以下のような特徴があります:
- Store: オンチェーンデータベース。各アプリで特定のデータモデリングやガスの最小化を行う必要がありません。
- World: アクセス制御、アップグレード、モジュールを提供するエントリーポイントカーネル。
- Foundryに基づく高速な開発ツール。
- オンチェーンの状態を反映するクライアントサイドのデータストア。Contractデータを取得するためにビュー関数やイベントを使用する必要はありません。
- MODE: SQLでクエリ可能なPostgresデータベース。オンチェーン状態を1対1で反映します。もうインデクサを書く必要はありません。
MUDは最大限にオンチェーン化されており、全てのアプリケーションの状態はEVM内に存在します。クライアントやフロントエンドに必要なのは、Ethereum NodeかMODE(MUDノード)だけです。すべてのMUDライブラリをフレームワークとして使用したり、必要な部分だけを選んだりすることができます。
MUDはEthereumアプリケーションのためのフレームワークで、EVM(Ethereum Virtual Machine)アプリケーションの構築の複雑さを圧縮します。MUDはオンチェーンデータベース、エントリーポイントフレームワーク、高速な開発ツール、クライアント側データストア、そしてSQLでクエリ可能なPostgresデータベースを含みます。
MUDはロールアップやチェーンではなく、オンチェーンアプリケーションを構築するためのライブラリとツールのセットです。また、MUDはEthereumメインネットに特化しているわけではなく、任意のEVM互換チェーンで動作します。
MUDの主要なアイデアの一つは、全てのオンチェーン状態がStore、つまりMUDのオンチェーンデータベースに保存されることです。現在のスマートコントラクトの状態管理方法は、いくつかの大きな問題を引き起こす可能性があります。たとえば、状態とロジックの結びつきがロジックのアップグレードを困難にし、SolidityとVyperのマッピングのキーのハッシュ化がスマートコントラクトのストレージを見ることができなくなります。
MUDでは、Solidityのコンパイラによるデータストレージは使用せず、すべての状態はStoreというガス効率の良いオンチェーンデータベースを使用して保存および取得されます。これはYulで手動で最適化された組み込みデータベースで、テーブル、列、行があります。この方法で、一般的な非MUDのSolidityではmapping(uint => mapping(uint => address))として実装されるデータ構造を実装することができます。
Storeはまた、特定のテーブルや行にフックを登録して、自動的にインデックス化されたビューを作成することも可能です。これにより、ロジックの一部がトークンの所有者を変更するたびに、Storeが自動的に対応するアドレスの行を再計算できるようになります。
さらに、MUDはWorldと呼ばれるエントリーポイントカーネルを使用することを推奨しています。Worldは、異なるContractからのStoreへのアクセスを仲介する役割を果たします。これにより、ロジックを簡単にアップグレードできるようになります。
最後に、MUDはSubgraphsやIndexersを書く必要がなく、フロントエンドは自動的に同期されます。これは、MUDのStore(そしてWorld)を使用することで、オンチェーンデータが自動的に調査可能になり、変更が標準イベントを通じて通知されるためです。
以上の特徴により、MUDはEthereumアプリケーションの開発を大幅に簡略化し、効率化することが可能です。
// table definition
Allowance: {
keySchema: {
from: "address",
to: "address",
},
schema: {
amount: "uint256",
},
}
// storing
AllowanceTable.set(address(0), address(1), 10);
// getting
allowance = AllowanceTable.get(address(0), address(1));
ETHGlobal Autonomous Worlds Hackathon
下記のようなアイディアの紹介があります
ゲーム名 | 概要 | ゲームの目標 | 主な特徴 | なぜオンチェーン |
---|---|---|---|---|
Client agnostic chess | チェスのロジック全体がチェーン上に存在するゲーム | Clientにあるそれぞれのロジックからトランザクションを送信してプレイする | 最初のプレイヤーが誰かとプレイするためのインターフェースを構築する必要がある | 相互運用性を最優先に置くため。既存のものの上に誰かが構築する必要がある |
Snake onchain | チェーン全体のスネークゲーム | 生き残ること | スネークが食べ物を食べると勝つ/サイズが増加する | 自動化の可能性。他のスネークに当たらないようにフロントランニング戦争がある |
Coin war | すべてのお金を手に入れようとするゲーム | 最も多くの$MONEYを得ること | 各プレイヤー(ウォレット)は自分のコイン10,000枚からスタート、それを沢山の人に配った人がMONEYを手に入れることができる。 | 囮と他人にコインを押し付けることが全て。プレイヤーが作成するコインには価値がなく、本当に希少なものだけが価値を持つことをルール化できる |
Not Monopoly | 所有権が流動的な不動産はどうなるのか | より多くの$MONEYを稼ぐこと(暗黙的な目標) | 32x32のグリッド。ターン制。 | ゲームは決して終わらない。 |
Crypters | 生きていて相互作用が必要なデジタルクリーチャー | 無し。クリーチャーは相互作用が必要 | 各クリーチャーはユニークで、ユニークな視覚がユニークな特性を反映している | 基本的な相互作用が行われないとクリーチャーは死んだり休眠状態に入ったりする |
DeFi-in-a-box | ユーザーが簡単にDeFiの基本を作成できるツール | 最も多くの$OGを作ること | 自動市場メーカー | Defi |
install & sample run
Global にインストールしたくないので、ローカルにインストールして試してみます。
% npm init
% yarn add pnpm
% npx pnpm create mud@canary test1-project
% cd test1-project
% npx pnpm run dev
...
[client]
[client] VITE v4.2.1 ready in 1235 ms
[client]
[client] ➜ Local: http://localhost:3000/
[client] ➜ Network: use --host to expose
...
[contracts] {
[contracts] worldAddress: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
[contracts] blockNumber: 3
[contracts] }
[contracts]
[contracts] .------..------..------.
[contracts] |M.--. ||U.--. ||D.--. |
[contracts] | (\/) || (\/) || :/\: |
[contracts] | :\/: || :\/: || (__) |
[contracts] | '--'M|| '--'U|| '--'D|
[contracts] '------''------''------'
[contracts]
[contracts] MUD watching for changes...
無事立ち上がります これでContractとWeb両方開発できる状態です
ボタンを押すと、ContractにTransactionが発生してStoreにデータが書き込まれ
それが、Webの表に反映されるという FullOnchainのデータ更新が簡単に実現できました! すごい。
Contract Code
自動生成されたContractのなかで下記が一番重要なconfigです
import { mudConfig } from "@latticexyz/world/register";
export default mudConfig({
tables: {
Counter: {
keySchema: {},
schema: "uint32",
},
},
});
このconfigにそって下記にContractが作成されています
library Counter {
/** Get the table's schema */
function getSchema() internal pure returns (Schema) {
SchemaType[] memory _schema = new SchemaType[](1);
_schema[0] = SchemaType.UINT32;
return SchemaLib.encode(_schema);
}
function getKeySchema() internal pure returns (Schema) {
SchemaType[] memory _schema = new SchemaType[](0);
return SchemaLib.encode(_schema);
}
/** Get the table's metadata */
function getMetadata() internal pure returns (string memory, string[] memory) {
string[] memory _fieldNames = new string[](1);
_fieldNames[0] = "value";
return ("Counter", _fieldNames);
}
Storeをマネージする唯一のWorldContractから受け取るSystemContractを定義しているのが下記です。
import { System } from "@latticexyz/world/src/System.sol";
import { Counter } from "../codegen/Tables.sol";
contract IncrementSystem is System {
function increment() public returns (uint32) {
uint32 counter = Counter.get();
uint32 newValue = counter + 1;
Counter.set(newValue);
return newValue;
}
}
Client src
export const App = () => {
const {
components: { Counter },
systemCalls: { increment },
network: { singletonEntity },
} = useMUD();
const counter = useComponentValue(Counter, singletonEntity);
return (
<>
<div>
Counter: <span>{counter?.value ?? "??"}</span>
</div>
<button
type="button"
onClick={async (event) => {
event.preventDefault();
console.log("new counter value:", await increment());
}}
>
Increment
</button>
</>
上記で呼び出しているincrement():systemCalls は下記でsetupされています。
export function createSystemCalls(
{ worldSend, txReduced$, singletonEntity }: SetupNetworkResult,
{ Counter }: ClientComponents
) {
const increment = async () => {
const tx = await worldSend("increment", []);
await awaitStreamValue(txReduced$, (txHash) => txHash === tx.hash);
return getComponentValue(Counter, singletonEntity);
};
return {
increment,
};
}
今回は ECS というコンポーネントは使わないそうなので下記のように書き換えて動作することを確認します。
import { useRow } from "@latticexyz/react";
export const App = () => {
const {
systemCalls: { increment },
network: { storeCache },
} = useMUD();
//const counter = useComponentValue(Counter, singletonEntity);
const counter = useRow(storeCache,{table:"Counter",key:{}});
storeにデータを追加
下記のようにTableの定義を追加して保存されると対応したBalanceTable.solが生成されます。
tables: {
...
BalanceTable: {
schema: "uint32",
keySchema: {
owner: "address",
item: "uint32"
}
},
},
Systemは自動生成ではなく、実現したいことに応じて自分でつくるものなので下記のように作成します
import { System } from "@latticexyz/world/src/System.sol";
import { BalanceTable } from "../codegen/Tables.sol";
contract MintSystem is System {
function mint(uint32 item) public {
uint32 balance = BalanceTable.get(_msgSender(), item);
balance = balance + 5;
BalanceTable.set(_msgSender(),item,balance);
}
}
上記で msg.sender とせずに _msgSender()としているのはSystemはWorldに呼び出されるのでもともとのsenderを取得するためです。
下記のようにweb2 client側のコードを書き換えると無事に読み書きできることが確認できました。(これはたしかに Onchaineの実装が素早くすすむことを実感です)
const mint = async () => {
const tx = await worldSend("mint", [1]);
};
const balances = useRows(storeCache,{table:"BalanceTable"});
return (
<>
<button onClick={mint}>Mint</button>
<>{balances.map(balance => <p>{balance.key.owner.substring(0,10)}has{balance.value.value}</p>)}</>
</>
);
deploy test net
- まずテスト用のWalletを作成(foundryのcast コマンド)
(base) contracts % cast wallet new
Successfully created new keypair.
Address: 0x91DcEFaAC85d6a7fE230180C91b8A52451b47d1f
-
.env にPRIVATE_KEYを貼り付ける。
-
このwalletにfaucetで補充
% npx pnpm mud faucet --address 0x91DcEFaAC85d6a7fE230180C91b8A52451b47d1f
(node:26699) ExperimentalWarning: Importing JSON modules is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Dripping to 0x91DcEFaAC85d6a7fE230180C91b8A52451b47d1f
Success
- deploy
% npx pnpm deploy:testnet
...
Deployment completed in 27.958 seconds
Deployment result (written to ./worlds.json and deploys/4242):
{
worldAddress: '0x09B163c272D96682F682d29dc5BcF326aea42228',
blockNumber: 14803239
}
下記のように URLにchainId parameterを加えることで
接続先のNetworkをLocalからtestnetに変更することができます
store
Storeはオンチェーンデータベースで、MUDを使用して開発する際には、開発者がContractの状態をStoreに保存します。これはSolidityのコンパイラによるデータストレージを置き換えます。
Storeは組み込み型のEVMデータベースであり、変数、マップ、配列などのアプリケーションデータをすべてStoreに格納できます。また、SQLデータベースで実装可能な任意のデータモデルをStoreに表現することができます。
また、Storeは自己説明的です。他のスマートコントラクトやオフチェーンアプリケーションは、標準的なデータ形式とEVMイベント形式を使用して任意のStoreのスキーマ、テーブル、レコードを探索できます。これにより、インデックス作成やフロントエンドネットワーキングをゼロコードで実現できます。
- data is stored in tables
- every table has a schema
- every record has a key
さらに、Storeはガス効率が良いです。それはSolidityのコンパイラによるストレージに対して保守的な制限を導入し、更に密なストレージエンコーディングを可能にすることで、一部の条件下でNative Solidityよりも安価なストレージを実現します。
Storeは、タプルキーのカラム型データベースです。テーブルは値カラムとキーカラムの二つの異なる種類のカラムを持ちます。レコードは、その全てのキーカラムを提供することで読み取りや書き込みが行われ、これらはプライマリキーとして考えることができます。
tables: {
MyTableWithTwoKeys: {
schema: {
value1: "uint32",
value2: "uint32",
},
keySchema: {
key1: "uint256",
key2: "string"
}
},
}
また、StoreはSolidityのコンパイラによるストレージとは異なり、テーブルは実行時に作成されます。デフォルトのテーブルはContractのコンストラクタで作成することができ、追加のテーブルはContractの寿命中に作成することが可能です。
Storeは以下のようなオンチェーンアプリケーションの一部の困難な状態関連の問題を解決する試みです:
- 状態とロジックの分離
- データへのアクセス制御
- フロントエンドとの同期化
- 他のContractからのデータクエリ
これらの問題を解決するために、Storeは4つの原則に基づいて設計されています:
-
Storeを使用するアプリケーションのContractストレージは自己説明的でなければならない: オフチェーンのインデクサ、フロントエンド、または他のContractは、Storeで見つかるデータ構造を発見し、そのスキーマを取得し、データの任意の有効な部分集合をクエリすることができるべきです。Store特有のABIファイルは不要です。
-
ストレージはEVMで最も高価なリソースであるため、Storeはそれを効果的に使用する必要があります: すべてのレコードは可能な限り密にパックされ、ストレージスペースを節約するべきです。Storeは、ストレージ管理でSolidityコンパイラを超越するために保守的な仮定を立てるべきです。
-
イベントを使用して、型、テーブルおよび列名、全レコードを含む完全なStoreを再構築できるべきです。これらのイベントは標準化され、ABIが必要なく、型付けされた方法でそれらをデコードするための十分な情報を含むべきです。
-
アプリケーションのすべてのストレージ(または複数のアプリケーション)は、ビジネスロジックがない単一のContractに集中化されるべきです。テーブルとレコードのレベルでアクセス制御を厳密に範囲指定できるべきです。
StoreとネイティブのSolidityストレージとの間にはいくつかの違いが存在します:
- StoreはSolidityコンパイラとは異なる方法でストレージをエンコードし、複数の配列を持つテーブルでガスの節約を実現します。
- Storeはネストされた構造体をサポートせず、テーブルごとに14の動的型(つまり、配列、文字列、バイト列)のみを許可します。これに対してSolidityはネストされた構造体を無制限の動的型でサポートします。
- Storeの配列は65,536要素に制限されていますが、テーブルごとに格納できるレコードの数には制限がありません。
World
"World"はMUDのカーネルで、以下を一緒にバンドルしたブロックチェーン上のアプリケーション作成の特定のパターンです:
- Store:MUDのブロックチェーン上のデータベース
- Systems:Storeのテーブルへの読み書きを行うステートレスなロジックContract
- 誰でもチェーン上でテーブルとSystemを許可なく作成する
- 名前空間に基づいてテーブルとSystemのアクセス制御を行う
- Modules:World上のテーブル、System、フックをインストールするブロックチェーン上のスクリプト
Worldカーネルを使用すると、アプリケーションのすべての状態とロジックが一つのContract(WorldContract)に集約されます。Worldは要求元によってStoreへの書き込みを調停し、機能呼び出しを対応するSystemにルーティングします。これらのSystemは次にWorld上のStoreへ読み書きします。
(例)一つのWorldContractのなかに複数のネームスペースごとのテーブルのイメージ
ルーターおよび一元化された状態のストレージとしての役割では、WorldはDiamondまたはProxyに似ています。これらのパターンとWorldフレームワークとの主な違いは、Worldが新しいロジックとテーブル(=状態)をWorld上に許可なしで登録することを可能にすることです。これは、ロジックの追加とアップグレードを許可するために管理者を必要とするDiamondとProxyとは対照的です。
WorldはStoreのストレージアクセス管理を持っているため、新しいロジックと新しいテーブルをアプリケーションに追加してもセキュリティリスクを導入することなく、その要件を緩和し、誰でも追加することができます。
これにより、必ずしも互いを信頼していない複数のアクターが同じStore上で構築し、テーブルとSystem間での相互運用性を可能にします。
もちろん、これは複数のContract間で行うことも可能で、例えば承認されていればDeFiコントラクトAがDeFiコントラクトBのERC-20バランスを転送することができます。
world 101(入門)
「World」はMUDの中核をなす部分で、以下の要素を束ねたオンチェーンアプリケーションを作成する特定のパターンです:
- Store:MUDのオンチェーンデータベース
- Systems:Storeのテーブルへ読み書きするステートレスロジックContract
- テーブルとSystemの無許可作成機能
- ネームスペースに基づいたテーブルとSystemへのアクセス制御
- Modules:World上にテーブル、System、フックをインストールするオンチェーンスクリプト
Worldカーネルを使用すると、アプリケーションのすべての状態とロジックが単一のContract、WorldContractに集約されます。Worldは、誰がそれらを要求するかに応じてStoreへの書き込みを仲介し、関数呼び出しをそれぞれのSystemにルーティングし、その後、それらのSystemはWorldのStoreへ読み書きします。
Worldは、ルーターおよび状態の集中化ストレージとしての役割を果たします。その構造は、ダイヤモンドやプロキシと似ていますが、Worldフレームワークが新たなロジックとテーブル(=状態)の無許可登録を可能にする点でこれらと異なります。この機能は、ダイヤモンドとプロキシがロジックの追加やアップグレードの許可に管理者を必要とするのと対照的です。
Worldはストレージアクセス管理を持っているため、新たなロジックと新たなテーブルをアプリケーションに追加するために、誰でもセキュリティリスクを引き起こすことなくその要件を緩和できます。
このことにより、必ずしも互いを信頼しない複数のアクターが同じStoreで構築し、そのテーブルとSystem間での相互運用性を可能にします。
Worldを使用せずにStoreを使用することも可能ですが、一つのContract下に状態を集約し、標準化されたアクセス制御メカニズムを強制することで、WorldフレームワークはWorldの全体的な状態を知ることができます
Worldの概念
-
リソースと名前空間: Worldにはリソースが存在し、現在のところリソースの種類は3つあります。これらはWorldのルートユーザーによって追加され、MUDの将来のバージョンでは新しいデフォルトのリソースが追加されるかもしれません。リソースは各々名前空間に含まれ、ワールド内のリソースはファイルシステムとして考えることができます。
-
System: シSystemはWorld上で実行される状態のないロジックで、名前空間内のリソースとして表現されます。SystemはSolidityで書かれ、通常のスマートコントラクトと同様にEVMにコンパイルされます。
-
テーブル: テーブルはリソースの一種で、ランタイムにWorldにインストールされます。各テーブルの状態はWorldContractのストレージ内に表現されます。
Worldの利用方法
-
Worldを使用するためには、プロジェクトが適切なフォルダ構造を持ち、Contractフォルダのルートにmud.config.tsファイルが存在する必要があります。
-
プロジェクトを始めるには、最小限のテンプレートから始めることをおすすめします。次のコマンドを実行します:
pnpm create mud@canary your-project-name
-
新しいテーブルやSystemを追加するには、configを拡張するだけで可能です。
-
テストの記述: 作成したSystemが実際にDogにレコードを追加するかどうかを確認するテストを書くことができます。
以上がMUDWorldフレームワークの大まかな概要と利用方法です。
world config
下記の表には、Worldフレームワークの設定に関する詳細を列挙しています。
設定項目 | 説明 | 例 |
---|---|---|
namespace | リソースがデプロイされる名前空間を定義します。デフォルトはROOT名前空間です。 | "mud" |
excludeSystems | “system”を含む名前であってもデプロイしないSystemを定義します。 | ["System3", "System2"] |
worldContractName | IWorldインターフェイスを実装するプロジェクト内のContractの名前。デフォルトのWorld実装を変更したい場合に有用です。 | "CustomWorld" |
deploysDirectory | デプロイ後にデプロイメントの成果物を格納するフォルダーを定義します。 | "./mud-deploys" |
modules | モジュール定義の配列。モジュール定義はname, root(オプション), argsキーを持ちます。 | { name: "KeysWithValueModule", root: true, args: [resolveTableId("CounterTable")] } |
systems | プロジェクト内のSystemを定義します。名前はファイルの名前と一致します。 | { IncrementSystem: { name: "increment", openAccess: true } } |
tables | プロジェクト内の全てのテーブルを定義します。 | { CounterTable: { schema: { value: "uint32" } } } |
System設定には以下の属性があります:
設定項目 | 説明 | 例 |
---|---|---|
fileSelector | Systemのファイルセレクタを定義します。 | "SystemA.sol" |
openAccess | falseに設定されている場合、同じ名前空間のSystemとaccessListからリスト化されたアドレスまたはSystemだけがアクセス可能です。デフォルトはtrueです。 | true |
accessList | openAccessがfalseの場合に必要。配列内の各アドレスはこのSystemにアクセスを許可され、それを呼び出すことができます。 | ["0x123..", "0x456.."] |
sub system
Sub Systemは、名前空間内で再利用されるロジックをまとめるプライベートなSystemです。例えば、NFTプロジェクトを作成する際に、トークンの所有者と各アドレスの総バランスを表すOwnerTableとBalanceTableを利用する場合、ERC-721の仕様に従って、これらのテーブルを原子的に変更し、Contract上でTransferイベントを発生させる必要があります。
この転送ロジックはゲーム、抽選、オークションなど、あなたのコードの多くの箇所で使用されます。このようなロジックを一箇所にまとめてエラーを避けるために、Sub System(この場合ではTransferSystem)を作成し、openAccessをfalseに設定することで実現します。これにより、同じ名前空間内のSystemだけがこのSubSystemを通じて転送を実行でき、異なる名前空間のシステムや外部EOAが直接このシステムを呼び出してNFTを直接転送するセキュリティリスクがありません。
アクセス制御については、Worldフレームワークを使用すると、Systemは常にWorldから呼び出され、直接呼び出されることはありません。Worldがエントリーポイントとして使用され、アクセス制御を処理します。ここでは、Worldはnftgame名前空間内のシステムだけがTransferSystemを呼び出すことができるように制限します。ContractやEOAがこのContractを直接呼び出すと、Store自動生成ライブラリはmsg.senderへの読み書きを試みますが、その場合には適切なStoreやStore APIが存在しないため、Storeの状態を変更することはありません。
Modules
モジュールはWorld上で実行可能なオンチェーンスクリプトで、テーブル、System、フック、新たなエントリポイントをWorldにインストールしてその能力を拡張するために使用されます。現在は各モジュールのコードがプロジェクト内に存在する必要がありますが、NPMのようなオンチェーンモジュールレジストリの導入に向けて進行中です。
デフォルトでは、各Worldには二つのモジュールがインストールされています。CoreModuleはWorldに重要なテーブルを追加し、RegistrationModuleはその依存性とともにRegistrationSystemをWorldに追加します。
KeysInTableModuleとKeysWithValueModuleはオンチェーンのテーブル情報をインデックス化し、SnapSyncModuleとqueryではオンチェーンクエリが可能となります。これらのモジュールは特定のテーブル内のすべてのキーのクエリや、特定の値を持つすべてのキーのクエリを可能にします。
SnapSyncModuleはクライアントがWorldの状態をクエリし、高速に同期することを可能にします。
queryは特定の基準に一致するキーのリストを取得するためのシンプルなAPIを提供します。これはスタンドアローンのモジュールではなく、KeysInTableとKeysWithValueがインストールされている必要があります。
クエリは一連のクエリフラグメントから構成され、それぞれが特定のテーブルにあるキー、特定の値を持つキー、特定のテーブルにないキー、特定の値を持たないキーをフィルタリングします。
Mode
MODEは、MUDを使ったオンチェーンアプリケーションのためのオフチェーンインデクサで、PostgresDBを基盤としたストレージバックエンドを使用し、任意のMUDアプリとそのまま動作します。任意のEVM互換チェーンのMUDアプリケーションをインデクシング可能で、チェーンごとに複数のWorldをインデクシングできます。
オンチェーンアプリを構築する際には、状態の変更を意味する書き込みと、情報の取得を意味する読み取りが重要となります。オンチェーンアプリの読み取りは複雑で、Ethereumネットワークのクエリ能力は、完全に同期した状態のノードを持っていれば、EVMを使用してほぼすべての情報を探索できますが、これはスマートコントラクトに対応するアカウントの生のストレージスロットを見ることを意味します。これを回避する一つの方法は、スマートコントラクト上でビュー関数を提供することです。
ただし、ビュー関数には、チェーンからの状態の「全体像」を得るためにこれらを生成し、呼び出す必要があるという複雑さがあります。このため、アプリケーションのクライアントは、必要な数のRPCリクエストを処理するために専用のノードを運用する必要が急速に高まることがあります。
MODEは、オンチェーン状態の簡単な「読み取り」の問題に対する一つの解決策で、MODEが接続している任意のネットワーク上の任意のMUDアプリケーションの状態を「追跡」することができます。状態が追跡され、解析され、保存されると、MODEはAPIを公開し、クライアントが現在の全体的な状態を一度のRPC呼び出しで取得することができます。
MODEのアーキテクチャはモジュラーで拡張性があり、高レベルでは以下の機能に分割されます:
- チェーンからのイベントを取り込む
- チェーンからのイベントを保存し、整理する
- 公開API: ストレージレイヤのイベント情報を提供する
MODEは、Postgresをストレージバックエンドとして使用します。これはMUD V2でデータが保存される方法と非常によく対応しており、これがオンチェーンデータレイアウト設計の初期のインスピレーションとなりました。このように保存されたデータは、DBの世界から多くの利点を得られるため、PostgresをDBとして使用することは直感的な選択でした。MODEは、起動時にMODEに設定パラメータとして提供されるPostgresデータベースに、チェーンからインデクシングされたすべてのテーブルを保存します。
MODEの単一の実行インスタンスは、複数のチェーン(ネットワーク)とWorld(MUDアプリケーション)をインデクシングすることができます。これは、Worldごと、チェーンごとに大量のテーブルを意味するため、MODEはPostgresスキーマを使用してテーブルと対応するデータを分離します。スキーマは、多数のテーブルを持つ場合にデータベースをよりよく整理するのに役立ちます。MODEは、スキーマを使って3つのレベルの名前空間を操作します:
- MODEの名前空間 - mode(内部的なMODEの運用テーブルのみ)
- チェーンの名前空間(例:mode_1はEthereumメインネット、mode_371337はhardhatネットワーク用)
- Worldの名前空間(例:mode_1_0x0000000000000000000000000000000000000000名前空間は、chainID 1の0x0000000000000000000000000000000000000000にデプロイされたMUD Worldの全てのテーブルを含む)
クエリ/ユーザー側から見ると、名前空間は抽象化されますが、同期に使用されるPostgresの公開APIはNamespaceオブジェクト仕様を必要とします。これは単純に、クライアントがどの状態を同期したいのかをMODEが把握できるようにするためです。
Mode Architecture
MODEは、ブロックチェーン上のMUDアプリケーションからデータを取得し、それをデータベースに構造化して保存するSystemです。MODEは以下の主要なレイヤーで構成されています:
- IngressLayer:MUDイベントを検出し、適切に処理します。これには、新しいテーブルの作成、キー/値の名前の追加、レコードの挿入/更新/削除などが含まれます。
- QueryLayer:クライアントがMODEと対話するときに利用するRPCエンドポイントを公開します。ユーザーリクエストを受け取り、バリデートし、SQLクエリを構築し、バックエンドに対してクエリを実行します。データの取得や更新のストリーミングなどを行います。
- WriteLayerとReadLayer:それぞれ、データベースの書き込みと読み込みを行うAPIを公開します。
- DatabaseLayer:MODEが接続するデータベースとの低レベルのインタラクションを担当します。任意のクエリの実行や単一行のクエリなどを行います。
- Schema Cache:テーブルのスキーマ情報を保持し、更新時には常に最新の情報を提供します。
これらのレイヤーを通じて、MODEはMUDアプリケーションの状態を同期し、維持します。クライアントはGetState
エンドポイントを使用して特定のネームスペース(チェーンIDとWorldアドレスによって定義)での現在の状態を取得したり、StreamState
エンドポイントを使用して状態の変更をストリームで受け取ることができます。
さらに、MODEはMUDで状態の同期を行うために統合されており、多くのケースでアプリの状態の同期を無駄なRPC呼び出し無しに行うことができます。また、MODEはgRPCサーバーとHTTPサーバーのラッパーを使用してQueryLayer APIを公開しているため、これらのエンドポイントへ直接呼び出しを行うことも可能です。
Discussion