このチュートリアルでは、Arweave の分散型永続ストレージと AO のハイパー並列コンピュータの基礎から応用までを学びます。初心者にもわかりやすく、段階的に知識を積み上げていきながら、実践的なコード例を通じて理解を深めていきます。
Arweave は、一度支払いをすれば永続的にデータを保存できる革新的なブロックチェーンプラットフォームです。従来のクラウドストレージとは異なり、継続的な支払いや維持費は不要で、データは分散化されたネットワーク全体に冗長的に保存されます。
AO は Arweave の上に構築された分散型スーパーコンピュータで、独自のアクターモデルを採用し、無限のスケーラビリティを実現しています。ここでは、AO の基本概念からカスタム VM の開発、そして AO トークンエコノミクスまで幅広くカバーします。
ローカル環境構築
AO と Arweave の開発を始めるには、まずローカル環境を準備します。
ここでは、NodeJS v22 と Docker が必要になります。
git clone -b hotfix https://github.com/weavedb/ao-localnet.git
cd ao-localnet/wallets && ./generateAll.sh
cd ../ && sudo docker compose --profile explorer up
この Docker 設定を実行することで、以下のローカルノードが起動します。
- ArLocal: localhost:4000 - Arweave ローカルゲートウェイ
- GraphQL: localhost:4000/graphql - Arweave GraphQL インターフェース
- Scar: localhost:4006 - Arweave ローカルエクスプローラ
- MU: localhost:4002 - AO メッセンジャーユニット
- SU: localhost:4003 - AO スケジューラユニット
- CU: localhost:4004 - AO コンピュートユニット
次に、AOS の Wasm モジュールをダウンロードし、AO ユニットに必要なウォレットを生成します。
nvm use 22
cd ao-localnet/seed && ./download-aos-module.sh
./seed-for-aos.sh
cd ../wallets && node printWalletAddresses.mjs
最後のコマンドでウォレットアドレスがリストされますので、スケジューラウォレットのアドレスをメモしておいてください。後の工程で使用します。
Arweave入門
この章では、Arweave の基本的な仕組みと操作方法を学びます。
Arweave は「一度アップロードすれば、永遠に消えない」データ保存を可能にする、ブロックチェーンベースの分散型ストレージです。
Arweaveとは
Arweave は「永続ウェブ」(Permaweb)を実現するブロックチェーンベースの分散型ストレージネットワークです。
従来のブロックチェーンや分散型ストレージと異なる主な特徴は以下の通りです。
-
一度の支払いで永続保存: データの保存に一度支払えば、そのデータは永続的に保存されます(継続的な支払いは不要です!)
-
ブロックウィーブ構造: 伝統的なブロックチェーンの直線的な構造とは異なり、Arweave は「ブロックウィーブ」と呼ばれる構造を使用しています。より効率的なデータアクセスと検証が可能になります
-
SPoRA(Succinct Proofs of Random Access): マイナーはランダムに選択された過去のデータにアクセスできることを証明することで、ネットワークのセキュリティに貢献します。これにより、データが実際に保存され、アクセス可能であることが保証されます
-
データの永続性と可用性: Arweave は「忘れない」ネットワークとして設計されています。一度保存されたデータは永続的に保存され、改ざんから保護されます
-
経済的持続可能性: Arweave は「エンダウメント」メカニズムを通じて、時間の経過とともにデータの保存コストが減少することを利用して、永続的なデータ保存の経済的持続可能性を確保しています
これらの特徴により、Arweave は特にアーカイブ、検閲耐性のあるコンテンツ、長期的な歴史的記録の保存などに適しています。
Arweave ゲートウェイへの接続
まず、Arweave の JavaScript ライブラリをインストールします。
npm i arweave
次に、ローカル環境の Arweave ゲートウェイに接続します。
const arweave = require("arweave").init({ port: 4000 })
メインネットに接続する場合は、デフォルト設定のまま使用できます。
const arweave = require("arweave").init()
Arweave ウォレットの作成
Arweave アカウントには RSA が使われていて、アドレスは公開鍵の SHA-256 ハッシュで 43 文字です。
JWK 形式で生成されたものを使うのが一般的です。
const addr = async jwk => arweave.wallets.jwkToAddress(jwk)
const gen = async () => {
const jwk = await arweave.wallets.generate()
return { jwk, addr: await addr(jwk) }
}
JWK は以下のフォーマットで生成されます。
{
"kty":"RSA",
"n":"o1kvT...",
"e":"AQAB",
"d":"Ohpdn...",
"p":"ymLt9...",
"q":"zp7X_...",
"dp":"pyN6W...",
"dq":"wkpHn....",
"qi":"Zvbxf..."
}
ここで、n
が公開鍵、d
が秘密鍵です。その他のフィールド(p
, q
, dp
, dq
, qi
)は暗号処理を高速化するためのパラメータです。
これらは Web Crypto API などと組み合わせて、署名や暗号化など様々な用途に使用できます。
AR トークンをミント
ローカル環境(ArLocal)では、AR トークンを無制限にミントできます。
ローカルノードでは、トランザクションをマイニングするために /mine
API を手動で呼び出す必要があります。
const mine = async () => await arweave.api.get(`/mine`)
const bal = async addr => {
return arweave.ar.winstonToAr(await arweave.wallets.getBalance(addr))
}
const mint = async (addr, amount = "1.0") => {
await arweave.api.get(`/mint/${addr}/${arweave.ar.arToWinston(amount)}`)
await mine()
return await bal(addr)
}
使用例:
const {jwk, addr} = await gen()
const balance = await mint(addr, "10.0")
console.log(`新しいウォレットの残高: ${balance} AR`)
AR トークンを送金
トランザクションの基本フローは createTransaction
、sign
、post
です。
送金の場合、target
と quantity
を指定します。
const postTx = async (tx, jwk) => {
await arweave.transactions.sign(tx, jwk)
await arweave.transactions.post(tx)
await mine()
return tx.id
}
const transfer = async (amount, to, jwk) => {
let tx = await arweave.createTransaction({
target: to,
quantity: arweave.ar.arToWinston(amount),
})
return await postTx(tx, jwk)
}
使用例:
const { addr: addr1, jwk } = await gen()
const { addr: addr2 } = await gen()
await mint(addr1, "1.0")
const txid = await transfer("0.5", addr2, jwk)
console.log(`送金トランザクションID: ${txid}`)
データを保存する
Arweave ではデータを直接保存することもできます。
テキストデータを保存する例を見てみましょう。
const saveMD = async (md, jwk) => {
let tx = await arweave.createTransaction({ data: md })
tx.addTag("Content-Type", "text/markdown")
return await postTx(tx, jwk)
}
使用例:
const txid = await saveMD("# これはマークダウンです", jwk)
console.log(`保存されたマークダウンのID: ${txid}`)
画像データも同様に保存できます。
const saveSVG = async (svg, jwk) => {
let tx = await arweave.createTransaction({ data: Buffer.from(svg, "base64") })
tx.addTag("Content-Type", "image/svg+xml")
return await postTx(tx, jwk)
}
// Base64エンコードされたSVGデータ
const ao = "PHN2ZyB3aWR0aD0iNDI5IiBoZWlnaHQ9IjIxNCIgdmlld0JveD0iMCAwIDQyOSAyMTQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0wIDIxNEg3MS4zNzYzTDg1Ljk0MjkgMTc0LjYxTDUzLjE2ODEgMTA3LjVMMCAyMTRaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTg5LjM2NiAxNjAuNzVMMTA5Ljk3OCAxTDg1Ljk0MjkgNTUuNzA4OUwxNjAuOTYxIDIxNEgyMTVMMTg5LjM2NiAxNjAuNzVaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTMyMiAyMTRDMzgxLjA5NCAyMTQgNDI5IDE2Ni4wOTQgNDI5IDEwN0M0MjkgNDcuOTA1NSAzODEuMDk0IDAgMzIyIDBDMjYyLjkwNiAwIDIxNSA0Ny45MDU1IDIxNSAxMDdDMjE1IDE2Ni4wOTQgMjYyLjkwNiAyMTQgMzIyIDIxNFpNMzIyIDE3MkMzNTcuODk5IDE3MiAzODcgMTQyLjg5OSAzODcgMTA3QzM4NyA3MS4xMDE1IDM1Ny44OTkgNDIgMzIyIDQyQzI4Ni4xMDEgNDIgMjU3IDcxLjEwMTUgMjU3IDEwN0MyNTcgMTQyLjg5OSAyODYuMTAxIDE3MiAzMjIgMTcyWiIgZmlsbD0iYmxhY2siLz4KPC9zdmc+Cg=="
const txid = await saveSVG(ao, jwk)
console.log(`保存されたSVG画像のID: ${txid}`)
GraphQL でトランザクションを取得
Arweave の最大の強みの一つは、トランザクションにデータと共に任意のタグを指定でき、GraphQL で柔軟に検索できることです。
これにより、分散型アプリケーションの構築が容易になります。
const q = txid => `query {
transactions(ids: ["${txid}"]) {
edges {
node { id tags { name value } owner { address } }
}
}
}`
const getTx = async txid => {
const json = await fetch("http://localhost:4000/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: q(txid) }),
}).then(r => r.json())
return json.data.transactions.edges.map(v => v.node)[0]
}
使用例:
const tx = await getTx(txid)
console.log(tx)
保存されたデータを取得するには、以下のいずれかの方法を使用します。
// arweave.js を使用する方法
const data = await arweave.transactions.getData(
txid,
{ decode: true, string: true}
)
// HTTP リクエストを使用する方法
const data = await fetch(`http://localhost:4000/${txid}`).then((r)=> r.text())
トランザクションをネストしてバンドル
Arweave では ANS-104 Bundled Data v2.0 規格を使用して、一つのトランザクション内に複数のトランザクションをネストできます。
このバンドル機能により、Arweave は無制限にスケールできます。
npm i arbundles
const { ArweaveSigner, bundleAndSignData, createData } = require("arbundles")
const bundle = async (_items, jwk) => {
const signer = new ArweaveSigner(jwk)
const items = _items.map(v => {
let tags = []
for (const k in v[1] || {}) tags.push({ name: k, value: v[1][k] })
return createData(v[0], signer, { tags })
})
const bundle = await bundleAndSignData(items, signer)
const tx = await bundle.toTransaction({}, arweave, jwk)
await postTx(tx, jwk)
return { items, tx }
}
使用例:
const { items, tx } = await bundle(
[
["# これはマークダウンです!", { Content_Type: "text/markdown" }],
[Buffer.from(ao, "base64"), { Content_Type: "image/svg+xml" }],
],
jwk
)
console.log(`バンドルトランザクションID: ${tx.id}`)
バンドル機能は AO プロトコルの基盤技術の一つとなっています。
AO入門
この章では、AO(Actor Oriented)という新しい分散型コンピューティングの基盤について学びます。
AOは、従来のスマートコントラクトとは異なる設計思想を持ち、無制限にスケール可能な分散型スーパーコンピュータとして注目を集めています。
AO とは
AO は「アクターオリエンテッド」(Actor Oriented)の略で、Arweave 上に構築された分散型ハイパー並列スーパーコンピュータです。
従来のブロックチェーン上のスマートコントラクトとは異なり、AO は無制限に水平スケールする能力を持っています。
AO の主な特徴は以下の通りです。
- 並列処理: 多数のプロセスが並行して実行可能
- メッセージパッシング: アクター間の通信はすべてメッセージを介して行われる
- スケーラビリティ: 理論上無制限にスケールできる設計
- モジュラリティ: 様々な言語や VM の実装が可能
- Arweave の永続性: すべてのデータと状態が Arweave 上に永続的に保存される
AO は単なるプロトコルであり、その実装はオープンで多様です。
AOS はその最初の実装の一つで、Lua 言語をベースにしています。
アクターモデルとメッセージング
AO は「アクターモデル」と呼ばれるコンピューティングパラダイムを採用しています。
アクターモデルは、1973年にカール・ヒューイットによって提案されたもので、並行計算の基本単位として「アクター」を使用します。
アクターモデルの主な特徴:
- アクターとは: 計算の基本単位で、独自の状態とメールボックスを持つ
- メッセージパッシング: アクター間のすべての通信はメッセージを介して行われる
- 非同期性: メッセージ処理は非同期的に行われる
- 独立した状態: 各アクターは自身の状態を持ち、他のアクターからは直接アクセスできない
- 生成能力: アクターは新しいアクターを生成できる
従来のオブジェクト指向プログラミングとの主な違いは、アクターモデルでは共有メモリが存在せず、すべての通信がメッセージを介して行われる点です。
これにより、並列計算の複雑さが大幅に軽減されます。
AO のメッセージングシステムでは、各プロセス(アクター)が他のプロセスにメッセージを送信でき、そのメッセージに応じて処理を行います。
メッセージには、データと任意のタグを含めることができます。
AO のアーキテクチャ
AO は以下の3つの主要コンポーネントから構成されています。
- メッセンジャーユニット (MU): メッセージの受信、検証、転送を担当
- スケジューラユニット (SU): メッセージの順序付けと Arweave への保存を担当
- コンピュートユニット (CU): メッセージを処理し、プロセスの状態を更新
これらのコンポーネントは疎結合されており、互いに独立して動作します。
これにより、システム全体の柔軟性と拡張性が向上します。
データフローは以下のようになります。
- ユーザーが MU にメッセージを送信
- MU がメッセージを検証し、適切な SU に転送
- SU がメッセージに順序を付け、Arweave に保存
- CU がプロセスの状態を計算し、結果を返す
- 必要に応じて、プロセスからのアウトプットメッセージが MU によって処理される
この設計により、AO は高いスケーラビリティと柔軟性を実現しています。
Wasm モジュールの基本
AO のプロセスは、WebAssembly(Wasm)モジュールをベースにしています。
Wasm は、高速なバイナリフォーマットで、多くのプログラミング言語からコンパイルできるため、AO の言語非依存性を実現しています。
Wasm の主な特徴:
- 高速実行: ネイティブに近い実行速度
- 小さなバイナリサイズ: 効率的なネットワーク転送
- 言語非依存性: C/C++、Rust、Go など多くの言語からコンパイル可能
- サンドボックス環境: 安全な実行環境
- メモリ制限: Wasm32 では 4GB、Wasm64 では 16GB のメモリ制限
AOS は Lua 言語を Wasm にコンパイルして使用していますが、他の言語や VM の実装も可能です。実際、このチュートリアルの後半では JavaScript ベースの独自 VM を実装します。
AO の経済モデル
AO トークンは、ネットワークのセキュリティとインセンティブ構造の中心です。
以下の特徴があります。
- 供給上限: ビットコインと同様に、2100万トークンの上限があります
- 自動分配: トークンは5分ごとに自動的に分配されます
-
分配比率:
- 33.3% が AR トークン保持者に
- 66.6% が aoETH 保持者に
- 半減期: ビットコインと同様の半減スケジュールに従います
AO トークンの主な目的は、ネットワークのセキュリティを提供し、参加者にインセンティブを与えることです。
具体的には、AO-Sec Origin プロセスによるステーキングと、SU/MU/CU をステークしたトークンにより、ネットワークのセキュリティが確保されます。
AO のセキュリティモデル
AO のセキュリティは、主に以下の2つのメカニズムに依存しています。
- AO-Sec Origin: 基本的なステーキングとセキュリティメカニズム
- Sybil-resistant Incentivized Validation (SIV): Sybil 攻撃に耐性のある検証メカニズム
AO-Sec Origin は、プロセスのホスティングや順序付けに関するセキュリティを提供します。
プロセスホストが「不正行為」をした場合(例:メッセージを正しく順序付けしない、プロセスの実行を拒否するなど)、ステークされたトークンがスラッシング(没収)される可能性があります。
SIV は、複数の独立したバリデーターが結果を検証するシステムで、Sybil 攻撃に耐性があります。
これにより、単一の悪意のあるノードが結果を操作することが困難になります。
これらのメカニズムにより、AO はセキュリティを確保しつつ、高いスケーラビリティを実現しています。
AOS入門
この章では、AO プロトコルの最初の実装である AOS(Actor Operating System) の開発方法を学びます。
AOS は Lua ベースの軽量なアクターランタイムで、アクターモデル × 永続ストレージ × 分散メッセージング を組み合わせた新しい実行環境です。
実際にコードを書きながら、プロセスの生成 → ロジックの登録 → メッセージ送受信 までをステップバイステップで体験していきます。
AOS とは
AOS(Actor Operating System) は、AO プロトコルの最初の実装として開発された、Lua ベースの分散型アクター実行環境です。
AO が定義するアクターモデルやメッセージング構造を忠実に実装し、誰でも簡単にアクター型アプリケーションを構築・デプロイできるように設計されています。
AOS は以下のような特徴を持ちます:
-
軽量な Lua ランタイム
- Lua は組み込み用途にも使われる非常に軽量なスクリプト言語で、Wasm へのコンパイルも容易です。
-
アクターモデルのフルサポート
- 各プロセスは独立した状態と受信ボックスを持ち、非同期メッセージで相互に通信します。
-
Arweave ベースの永続性
- プロセスの状態やメッセージ履歴は Arweave に永続保存され、任意のタイミングで復元可能です。
-
Eval による動的ロジック登録
- デプロイ後でもプロセスに Lua スクリプトを送信することで、ロジックを柔軟に拡張・変更できます。
-
スクリプトによる完全な自動化
-
aoconnect
ライブラリを通じて、プロセスの生成・操作・テストがすべて JavaScript から制御できます。
-
AOS は、AO の基本概念を理解し、自身の分散型アクターアプリを素早く構築するための理想的な入り口です。
この章では、AOS を使ったプロセス開発の一連の流れを、実際のコードとともに学んでいきます。
AOS のセットアップ
AOS は AO の最初の実装の一つで、Lua 言語を使用しています。
AOS を使用するには、まず aoconnect
ライブラリをインストールします。
npm i @permaweb/aoconnect
ユニットに接続
ローカル環境で AOS を使用するには、各ユニット(MU/SU/CU)の URL を指定して接続します。
const { createDataItemSigner, connect } = require("@permaweb/aoconnect");
const { result, message, spawn, dryrun } = connect({
MU_URL: "http://localhost:4002",
CU_URL: "http://localhost:4004",
GATEWAY_URL: "http://localhost:4000",
});
メインネットを使用する場合は、以下のようにシンプルに接続できます。
const { result, message, spawn, dryrun } = require("@permaweb/aoconnect");
AOS プロセスを生成
AOS プロセスを生成するには、AOS モジュールの ID とスケジューラウォレットアドレスが必要です。
以下は、プロセスを生成する基本的なコードです。
const wait = (ms) => new Promise((res) => setTimeout(() => res(), ms));
const pid = await spawn({
module: AOS_MODULE_TXID, // 先ほどメモしたモジュールのトランザクションID
scheduler: SCHEDULER_WALLET, // スケジューラウォレットのアドレス
signer: createDataItemSigner(jwk),
});
await wait(1000);
console.log(`生成されたプロセスID: ${pid}`);
AO プロセスが生成されると、そのプロセスは Arweave 上に永続的に存在し、メッセージを受け取って処理できるようになります。
Lua ハンドラーの基本
AOS では、Lua 言語を使用してプロセスのビジネスロジックを記述します。
特に重要なのは「ハンドラー」の概念です。
ハンドラーは、特定の条件に一致するメッセージを受け取ったときに実行される関数です。
AOS には以下のグローバル変数とモジュールが用意されています。
- Inbox: 未処理のメッセージを格納するテーブル
- Send: メッセージを送信する関数
- Spawn: 新しいプロセスを生成する関数
- Handlers: ハンドラーを管理するテーブル
- ao: プロセス情報やメッセージ送信などの機能を提供するモジュール
- json: JSON エンコード/デコード用モジュール
以下は、ハンドラーの基本的な構造です。
Handlers.add(
"ハンドラー名", -- ハンドラーの識別子
function(msg) -- マッチング関数: メッセージがこのハンドラーによって処理されるべきかを判断
return msg.Action == "特定のアクション" -- 条件に一致する場合は true を返す
end,
function(msg) -- ハンドラー関数: メッセージを処理する
-- メッセージを処理するロジック
ao.send({Target = msg.From, Data = "応答メッセージ"})
end
)
Handlers.utils
には、よく使われるマッチング関数が用意されています。
-- タグが一致するメッセージをマッチング
Handlers.utils.hasMatchingTag("Action", "Get")
-- データが一致するメッセージをマッチング
Handlers.utils.hasMatchingData("ping")
-- 応答を簡単に送信するユーティリティ
Handlers.utils.reply("pong")
ハンドラーを作成して登録
ここでは、簡単な Key-Value ストアを実装してみましょう。
以下の Lua コードを kv-store.lua
ファイルに保存します。
local ao = require('ao')
Store = Store or {}
Handlers.add(
"Get",
Handlers.utils.hasMatchingTag("Action", "Get"),
function (msg)
assert(type(msg.Key) == 'string', 'Key is required!')
ao.send({ Target = msg.From, Tags = { Value = Store[msg.Key] }})
end
)
Handlers.add(
"Set",
Handlers.utils.hasMatchingTag("Action", "Set"),
function (msg)
assert(type(msg.Key) == 'string', 'Key is required!')
assert(type(msg.Value) == 'string', 'Value is required!')
Store[msg.Key] = msg.Value
Handlers.utils.reply("Value stored!")(msg)
end
)
このコードを既存のプロセスに追加するには、以下のように Eval
アクションを使用します。
const { readFileSync } = require("fs");
const { resolve } = require("path");
const lua = readFileSync(resolve(__dirname, "kv-store.lua"), "utf8");
const mid = await message({
process: pid,
signer: createDataItemSigner(jwk),
tags: [{ name: "Action", value: "Eval" }],
data: lua,
});
const res = await result({ process: pid, message: mid });
console.log(res);
これにより、Key-Value ストアの機能がプロセスに追加されます。
メッセージの送受信
プロセスにメッセージを送信するには、message
関数を使用します。
const mid1 = await message({
process: pid,
signer: createDataItemSigner(jwk),
tags: [
{ name: "Action", value: "Set" },
{ name: "Key", value: "test" },
{ name: "Value", value: "abc" },
],
});
const res1 = await result({ process: pid, message: mid1 });
console.log(res1.Messages[0]);
読み取り専用のクエリには、dryrun
関数を使用すると効率的です。
const res2 = await dryrun({
process: pid,
signer: createDataItemSigner(jwk),
tags: [
{ name: "Action", value: "Get" },
{ name: "Key", value: "test" },
],
});
console.log(res2.Messages[0].Tags);
dryrun
は実際にメッセージを Arweave に保存せず、現在の状態を使用してシミュレーションするため、読み取り操作に適しています。
開発パターンとベストプラクティス
AOS 開発におけるベストプラクティスをいくつか紹介します。
-
ステート管理:
- グローバル変数を使用する場合は、初期化を忘れないようにする (
Store = Store or {}
) - 大きなデータ構造は、適切にネスト化して管理する
- グローバル変数を使用する場合は、初期化を忘れないようにする (
-
エラーハンドリング:
-
assert
を使用して入力を検証する - エラーメッセージは具体的かつ明確にする
-
-
モジュール化:
- 関連する機能をまとめる
- 再利用可能な関数を作成する
-
デバッグ:
-
ao.log
関数を使用してデバッグ情報を出力する - 複雑なデータ構造は
json.encode
でログに出力する
-
-
セキュリティ:
- 信頼できないデータは常に検証する
- 重要な操作は認証チェックを行う
-
テスト:
- テスト専用のプロセスを作成してテストする
-
dryrun
を使用して変更を確認してから本番環境に適用する
これらのプラクティスに従うことで、より安全で効率的な AOS アプリケーションを開発できます。
AO の大きな特徴の一つは、様々な言語や VM の実装が可能な点です。
ここでは、シンプルな JavaScript ベースの VM を独自に開発してみましょう。
応用: AOを活用した独自 VM の開発
この章では、AO プロトコルの柔軟性を示すために、独自の JavaScript ベースの仮想マシン(VM)を構築する方法を学びます。
AO は多言語・多種VMをサポートする設計になっており、既存のWasmだけでなく、カスタムVMを実装できる点が大きな特徴です。
この章を通じて、以下の内容を段階的に理解していきます。
- AO ローダーの役割と基本的な仕組み
- メッセンジャーユニット(MU)とコンピュートユニット(CU)の実装例
- JavaScript を使った簡単なカスタムVMの動作原理
- プロセスモジュールの登録から実際のプロセス生成とテストまでの流れ
これにより、AO の拡張性とカスタマイズ性の本質が理解できるようになります。
独自 VM 開発ハンズオン
AO ローダーの基本
AO のプロセス実行は、CU(コンピュートユニット)によって行われます。
CU はプロセスのモジュールをダウンロードし、AO ローダーを使用して実行します。
標準の AO ローダーは Wasm を実行しますが、独自のローダーを実装することも可能です。
ローダーの役割は、メッセージを受け取り、プロセスの状態を更新し、結果を返すことです。
AO のメッセージフォーマットに準拠していれば、どのような実装でも可能です。
Messenger Unit の実装
まず、シンプルな Messenger Unit(MU)を実装します。
MU の役割は、ユーザーからのメッセージを受け取り、検証し、適切な SU に転送することです。
const express = require("express")
const app = express()
const bodyParser = require("body-parser")
app.use(bodyParser.raw({ type: "application/octet-stream" }))
const port = 3000
const { DataItem } = require("arbundles")
const { connect } = require("@permaweb/ao-scheduler-utils")
const { validate, locate, raw } = connect({
GRAPHQL_URL: "http://localhost:4000/graphql",
cacheSize: 1000,
followRedirects: true,
GRAPHQL_MAX_RETRIES: 0,
GRAPHQL_RETRY_BACKOFF: 300,
})
const getTag = (name, tags) => {
for (const v of tags) {
if (v.name === name) return v.value
}
return null
}
const schs = {}
app.post("/", async (req, res) => {
const binary = req.body
let valid = await DataItem.verify(binary)
const item = new DataItem(binary)
const type = getTag("Type", item.tags)
let _url = null
if (type === "Process") {
const sch = getTag("Scheduler", item.tags)
const { url } = await locate(item.id, sch)
schs[item.id] = url
_url = url
} else {
_url = schs[item.target]
}
try {
const json = await fetch(_url, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: binary,
}).then(response => response.json())
console.log(json)
} catch (e) {
console.log(e)
}
res.json({ id: item.id })
})
app.get("/", async (req, res) => {
res.send("ao messenger unit")
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
このコードを mu.js
ファイルに保存し、実行します。
node mu.js
Compute Unit の実装
次に、シンプルな Compute Unit(CU)を実装します。
この CU は、JavaScript コードをサンドボックス環境で実行し、メッセージの Num
タグを入力として、現在のカウント値を出力します。
const vm = require("vm")
const express = require("express")
const app = express()
const bodyParser = require("body-parser")
app.use(bodyParser.raw({ type: "application/octet-stream" }))
const port = 3001
const { DataItem } = require("arbundles")
const { connect } = require("@permaweb/ao-scheduler-utils")
const { validate, locate, raw } = connect({
GRAPHQL_URL: "http://localhost:4000/graphql",
cacheSize: 1000,
followRedirects: true,
GRAPHQL_MAX_RETRIES: 0,
GRAPHQL_RETRY_BACKOFF: 300,
})
const getTag = (name, tags) => {
for (const v of tags) {
if (v.name === name) return v.value
}
return null
}
const q = txid => `query {
transactions(ids: ["${txid}"]) {
edges {
node {
id
tags { name value }
owner { address }
}
}
}
}`
let modules = {}
let schs = {}
let results = {}
let lasts = {}
app.get("/result/:mid", async (req, res) => {
const { ["process-id"]: pid } = req.query
const { mid } = req.params
const json = await fetch("http://localhost:4000/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: q(pid) }),
}).then(r => r.json())
if (!modules[pid]) {
const module = getTag("Module", json.data.transactions.edges[0].node.tags)
let js = await fetch(`http://localhost:4000/${module}`).then(r => r.text())
modules[pid] = { code: js, id: module, state: { count: 0 } }
}
if (!schs[pid]) {
const { url } = await locate(pid)
schs[pid] = url
}
let _url = `${schs[pid]}/${pid}`
if (lasts[pid]) _url += `?from=${lasts[pid]}`
const { page_info, edges } = await fetch(_url).then(r => r.json())
for (const v of edges) {
const mid = v.node.message.id
if (!results[mid]) {
const num = getTag("Num", v.node.message.tags)
const code =
modules[pid].code +
`
count = handle(count, ${num * 1});
`
const context = vm.createContext(modules[pid].state)
vm.runInContext(code, context)
modules[pid].state = context
results[mid] = context
lasts[pid] = v.cursor
}
}
res.json({
Messages: [
{
Tags: [{ name: "Count", value: Number(results[mid].count).toString() }],
},
],
})
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
このコードを cu.js
ファイルに保存し、実行します。
node cu.js
JavaScript モジュールの登録
次に、JavaScript モジュールを作成して Arweave にアップロードします。
このモジュールは、入力された数値を現在のカウントに加算する単純な関数です。
const arweave = require("arweave").init({ port: 4000 })
const js = `
function handle(count, num) {
return count + num;
}`
const { addr: addr1, jwk } = await gen()
const balance = await mint(addr1)
let tx = await arweave.createTransaction({ data: js })
tx.addTag("Data-Protocol", "ao")
tx.addTag("Variant", "ao.TN.1")
tx.addTag("Type", "Module")
tx.addTag("Module-Format", "js-unknown-unknown")
tx.addTag("Input-Encoding", "JSON-V1")
tx.addTag("Output-Encoding", "JSON-V1")
tx.addTag("Content-Type", "text/javascript")
const module_txid = await postTx(tx, jwk)
await wait(1000)
モジュールがアップロードされたら、スケジューラの URL を登録します。
let tx = await arweave.createTransaction({ data: "1984" })
tx.addTag("Data-Protocol", "ao")
tx.addTag("Variant", "ao.TN.1")
tx.addTag("Type", "Scheduler-Location")
tx.addTag("Url", "http://localhost:4003")
tx.addTag("Time-To-Live", "1000000000")
await postTx(tx, jwk)
await wait(1000)
プロセスの作成とテスト
最後に、モジュールを使用してプロセスを作成し、テストします。
const { createDataItemSigner, connect } = require("@permaweb/aoconnect")
const { result, message, spawn, dryrun } = connect({
MU_URL: "http://localhost:3000",
CU_URL: "http://localhost:3001",
GATEWAY_URL: "http://localhost:4000",
})
const pid = await spawn({
module: module_txid,
scheduler: addr,
signer: createDataItemSigner(jwk),
})
await wait(1000)
const mid = await message({
process: pid,
signer: createDataItemSigner(jwk),
tags: [{ name: "Num", value: "4" }],
})
const res = await result({ process: pid, message: mid })
console.log(res.Messages[0].Tags)
これにより、プロセスにメッセージが送信され、カウント値が増加します。
結果として、Count
が 4
のタグが含まれていれば成功です。
二回同じメッセージを実行すると 8
になります。
AOトークン設計
この章では、AOネットワークの経済的な仕組みを支える AOトークン設計の基本原則について学びます。
AOトークンは、ネットワークのセキュリティやエコシステムの発展に欠かせない重要な要素です。
特に、以下のポイントに焦点を当てて解説します。
- AOトークンの供給モデルと分配方法
- ARトークンとの関係性
- aoETHを含むクロスチェーン資産との連携
- 開発者に対するトークンインセンティブの仕組み
これらを理解することで、AOのトークンエコノミー全体の仕組みと、開発者やユーザーがどのように恩恵を受けるかが明確になります。
AO トークンの基本原則
AO トークンは、AO ネットワークの経済モデルの中心です。
その設計原則は以下の通りです。
-
完全フェアローンチ: AO トークンは、ビットコインのような完全なフェアローンチを採用しています。事前配布や ICO はありません
-
固定供給上限: ビットコインと同様に、21,000,000 トークンの供給上限があります
-
自動分配: トークンは5分ごとに自動的に分配されます
-
半減期: ビットコインと同様の半減スケジュールに従います。約4年ごとに発行量が半減します
-
分配比率: 発行されるトークンの 33.3% が AR トークン保持者に、66.6% が aoETH 保持者に分配されます
-
マイニング不要: 従来のブロックチェーンとは異なり、コンピュータリソースを使用したマイニングは不要です。代わりに、AR トークンや aoETH の保有量に応じてトークンが分配されます
AR と AO トークンの関係
AR トークンは Arweave ネットワークのネイティブトークンであり、データの永続的な保存に使用されます。
一方、AO トークンは AO ネットワークのネイティブトークンで、主にセキュリティとインセンティブ提供に使用されます。
AR トークン保持者は、保有量に応じて AO トークンの 33.3% を自動的に受け取ります。
これにより、Arweave のエコシステムと AO のエコシステムが緊密に連携します。
aoETH とブリッジされた資産
AO ネットワークでは、Ethereum や他のブロックチェーンからの資産をブリッジすることができます。
特に注目すべきは aoETH で、これは AO ネットワークにブリッジされた ETH です。
aoETH 保持者は、保有量に応じて AO トークンの 66.6% を自動的に受け取ります。
これにより、Ethereum のエコシステムと AO のエコシステムが連携し、クロスチェーンの流動性が促進されます。
aoETH の主な特徴:
- イーサリアムの流動性: ETH の流動性を AO ネットワークに持ち込むことができます。
- AO トークンの報酬: aoETH を保有することで AO トークンの報酬を受け取れます。
- クロスチェーン相互運用性: Ethereum のエコシステムと AO のエコシステムを橋渡しします。
開発者向けのトークンインセンティブ
AO の興味深い特徴の一つは、開発者向けのトークンインセンティブモデルです。
ユーザーが AR や aoETH をプロジェクトにロックすると、そのプロジェクトに AO トークンが自動的に分配されます。
これにより、開発者は以下のようなメリットを得られます。
- 持続可能な資金源: ユーザーがプロジェクトにトークンをロックすることで、継続的な資金源を確保できます
- ユーザーアライメント: ユーザーとプロジェクトの利益が一致します。ユーザーはプロジェクトの成功によって報酬を得ることができます
- コミュニティ主導の開発: トークンホルダーがプロジェクトの方向性に影響を与えることができます
このモデルにより、開発者はユーザーからの直接的な支援を受けながら、持続可能な方法でプロジェクトを開発することができます。
APPENDIX
これにてAO Bootcamp Vol.2 は終了となります。最後までお疲れさまでした!
以下は付録となります。
今後も引き続き、Arweave AOに関する学習リソースを公開していく予定です。次回はClaude Codeを活用した最新かつ実践的なdApp開発フローについてご紹介いたします。
Key-Value ストアの完全な例
以下は、完全な Key-Value ストアの実装例です。
// kv-store.js
const { createDataItemSigner, connect } = require("@permaweb/aoconnect")
const { readFileSync } = require("fs")
const { resolve } = require("path")
// ウォレットの読み込み
const jwk = JSON.parse(readFileSync(resolve(__dirname, "wallet.json")))
// AO ユニットに接続
const { result, message, spawn, dryrun } = connect({
MU_URL: "http://localhost:4002",
CU_URL: "http://localhost:4004",
GATEWAY_URL: "http://localhost:4000",
})
// 待機関数
const wait = ms => new Promise(res => setTimeout(() => res(), ms))
// メイン関数
async function main() {
// プロセスの生成
const pid = await spawn({
module: "YOUR_MODULE_ID", // AOS モジュールの ID
scheduler: "YOUR_SCHEDULER_WALLET", // スケジューラウォレットのアドレス
signer: createDataItemSigner(jwk),
})
await wait(1000)
console.log(`生成されたプロセスID: ${pid}`)
// Lua ハンドラーの定義
const lua = `
local ao = require('ao')
Store = Store or {}
Handlers.add(
"Get",
Handlers.utils.hasMatchingTag("Action", "Get"),
function (msg)
assert(type(msg.Key) == 'string', 'Key is required!')
ao.send({ Target = msg.From, Tags = { Value = Store[msg.Key] }})
end
)
Handlers.add(
"Set",
Handlers.utils.hasMatchingTag("Action", "Set"),
function (msg)
assert(type(msg.Key) == 'string', 'Key is required!')
assert(type(msg.Value) == 'string', 'Value is required!')
Store[msg.Key] = msg.Value
Handlers.utils.reply("Value stored!")(msg)
end
)
`
// ハンドラーの登録
const mid = await message({
process: pid,
signer: createDataItemSigner(jwk),
tags: [{ name: "Action", value: "Eval" }],
data: lua,
})
const res = await result({ process: pid, message: mid })
console.log("ハンドラー登録結果:", res)
// 値の設定
const mid1 = await message({
process: pid,
signer: createDataItemSigner(jwk),
tags: [
{ name: "Action", value: "Set" },
{ name: "Key", value: "test" },
{ name: "Value", value: "abc" },
],
})
const res1 = await result({ process: pid, message: mid1 })
console.log("値の設定結果:", res1.Messages[0])
// 値の取得
const res2 = await dryrun({
process: pid,
signer: createDataItemSigner(jwk),
tags: [
{ name: "Action", value: "Get" },
{ name: "Key", value: "test" },
],
})
console.log("値の取得結果:", res2.Messages[0].Tags)
}
main().catch(console.error)
トラブルシューティング
AO 開発における一般的な問題と、その解決策を紹介します。
-
メッセージの送信に失敗する:
- ウォレットの残高が十分か確認する
- 正しいプロセス ID を指定しているか確認する
- メッセージの形式が正しいか確認する
-
プロセスの状態が更新されない:
- ハンドラーが正しく設定されているか確認する
- メッセージのタグやデータが、ハンドラーの条件と一致しているか確認する
- CU がプロセスの状態を正しく計算しているか確認する
-
デバッグ情報の取得:
-
ao.log
関数を使用してデバッグ情報を出力する - ao.link エクスプローラを使用して、プロセスの状態とメッセージを確認する
- プロセスの結果を詳細に確認する
-
-
パフォーマンスの最適化:
- 大きなデータ構造の使用を避ける
- 不要なメッセージの送信を避ける
- 読み取り操作には
dryrun
を使用する
-
ネットワーク接続の問題:
- 各ユニット(MU/SU/CU)が正しく動作しているか確認する
- ネットワーク設定(URL、ポートなど)が正しいか確認する
- タイムアウト設定を調整する
AO 開発における重要なツールとして、ao.link エクスプローラがあります。
このツールを使用することで、プロセスの状態やメッセージの流れを視覚的に確認することができます。
参考資料
- Arweave ドキュメント
- AO スペック
- AO クックブック
- aoconnect ドキュメント
- ANS-104 仕様
- ao-scheduler-utils
- ao-loader
- AO ホワイトペーパー
- ao.link エクスプローラ
以上のリソースを活用することで、Arweave と AO の開発をより深く理解し、効率的に進めることができます。