Deep Dive Khaos

Aggre
所要時間, 12分
中級以上

Khaos

こんにちは、Aggre です。

この記事では、Dev Protocol のサイドプロジェクトとして開発されている Khaos のことを知ることができます。

Khaos とは、ブロックチェーン上に存在しない情報をブロックチェーンの外部からブロックチェーンに持ち込む "オラクル(神託)" の機能を担っています。そのため Khaos Oracle と呼ぶこともあります。

オラクルの必要性

Khaos に限らず、Ethereum には様々なオラクルプロトコルが存在します。Ethereum とは、スマートコントラクトによって定義されたプロトコルに基づいて発生する状態遷移の連鎖を保持した巨大なステートです。つまり入力トランザクションが存在しないデータは Ethereum 上に存在しないことになります。入力されるデータソースを HTTP から取得したい場合は、HTTP のレスポンスを手作業で入力するか、信頼できるボットによって自動的に入力させるかのどちらかとなり、オラクルとは後者のユースケースで使用されます。例えば "明日の東京の気温を当てた人が勝つブロックチェーンゲーム" を作るとき、"東京の気温データ" が必要になります。そのデータを天気マニアの Alice が手作業で入力する場合と、気象庁のデータベースから得られたデータをボットが入力するように記述されたスマートコントラクトと、どちらが信用できるでしょうか? オラクルは、後者を選ぶ場合に必ず必要になる技術です。

Khaos の必要性

オラクルの一般的な実装では、スマートコントラクトからイベントを emit して、そのイベントをオラクルプロトコルを構成するサーバーが検知し、最後にコールバック関数を呼び出す、という手法が採られます。emit するイベントのペイロードのなかにスマートコントラクトが要求している情報(例えば、"東京" の "気温" など)を付加することで、オラクルプロトコルはスマートコントラクトが何を欲しているのかを知ります。

Khaos はオラクルリクエストのためのペイロードを秘匿化することのできるオラクルです。オラクルしたい情報が何らかのシークレットトークンに基づいて取得する必要がある場合、パブリックなペイロードにシークレットトークンをそのまま含めることはできないので、シークレットトークンなどを公開可能な形式に換え安全にする必要があります。

Khaos を使うと、あなたのシークレットトークンを秘匿化したままで、パブリックブロックチェーン上でオラクルすることができます。

Khaos を使う

あなたの Dapps でも Khaos を使うことができます。フロントエンドのための SDK, オラクル関数を実装するための Starter Kit を使うことで、すぐに開発を始めることができます。

Khaos で使用可能なスマートコントラクトは現在、Dev Protocol のコアを構成するスマートコントラクトまたは Market, Policy コントラクトに限定されています。将来的にすべてのスマートコントラクトで使用可能になる予定です。

Khaos のオラクルフロー permalink

Khaos の開発を始める前に、そのオラクルフローを理解しておくのはいいスタートです。Khaos は Khaos Core, Khaos Functions, Khaos Registry などの複数の要素により成り立ちますが、Dapps 開発者にとって重要なのは以下のフローです。

  1. Khaos の Sign API(RESTful API) をコールして、秘匿化対象のデータを公開可能に変換した Public Signature を取得します。
  2. スマートコントラクトからイベントを emit する。このとき、イベントペイロードに Public Signature を含めることで、あなたが定義したオラクル関数は秘匿情報を取り扱うことができます。
  3. Khaos はあなたが定義したコールバック関数をコールして、フローを閉じます。

Khaos は多くのインターフェイスをユーザーに委ねることでオラクルに高い自由度を与えています。その代わりユーザーは多くのインターフェイスを自ら実装する必要がありますが、 Khaos Starter Kit を使うことですぐに開発を始めることができます。

Public Signature permalink

Khaos を扱ううえで重要なキーワードのひとつに Public Signature があります。

Public Signature とは Json Web Tokens によって暗号化された文字列ですが、暗号化に使われる情報はすべて公開可能な情報であり、誰でも生成、復号化できます。つまり Public Signature には秘密情報が一切含まれておらず、公開可能です。Khaos ではこの Public Signature をキーとして秘密情報を保存し、Khaos インスタンスの内部でのみ利用します。

Public Signature は以下のような JSON 文字列を送信者の Ethereum アカウントアドレスによって暗号化したものです。

{
"i": "...",
"m": "..."
}

実装とテストは Khaos Core で確認できます。
khaos-core/src/sign/publicSignature at main · dev-protocol/khaos-core (github.com)

Khaos Starter Kit permalink

dev-protocol/khaos-starter-kit: 🌌Start developing Khaos Functions now (github.com)

ユーザーが定義すべきインターフェイスのテンプレートを提供しています。

このリポジトリをフォークしたらクローンして、あなたのローカル環境で開発を始めましょう。Khaos Starter Kit は yarn でパッケージマネジメントしているので、事前に yarn をインストールしておく必要もあります。

$ git clone git@github.com:YOUR/khaos-starter-kit.git
$ cd khaos-starter-kit
$ yarn

src ディレクトリにはあなたが定義する必要のあるインターフェイスのテンプレート、そしてテストが TypeScript で書いてあります。

Khaos Starter Kit では、ESLint プラグイン eslint-plugin-functional をセキュリティのために推奨しています。あなたはそれを自由に変更できますが、なるべく変更せずに使うことをお勧めします。

abi.ts permalink

あなたのスマートコントラクトの ABI を Human-Readable ABI Format で記述した array を定義し、abi と命名してエクスポートします。

例えば Query というイベント、callback というコールバック関数を持つスマートコントラクトであれば以下のように書きます。Khaos が使用するのはイベントとコールバック関数だけなので、そのスマートコントラクトに他のインターフェイスがあってもすべてをここに定義する必要はありません。

import {Abi} from '@devprotocol/khaos-core'

export const abi: Abi = [
'event Query(string calldata fooId, string calldata publicSignature, address account, bytes32 queryId)',
'function callback(bytes32 queryId, bool result) external;'
]

addresses.ts permalink

オラクルリクエストを emit するスマートコントラクトのアドレスを Promise<string | undefined> で返す関数を定義し、addresses でエクスポートします。この関数の返却値はコールバック関数のアドレスとしても使用されます。

関数は引数として以下のようなオブジェクトを受け取ります。

type Options = {
readonly network: 'mainnet' | 'ropsten'
}

Ethereum のメインネットまたは Ropsten テストネットによってアドレスを切り分けることができます。

import {FunctionAddresses} from '@devprotocol/khaos-core'

export const addresses: FunctionAddresses = async ({network}) =>
network === 'mainnet'
? '0x1510EA12a30E5c40b406660871b335feA32f29A'
: '0x609Fe85Dbb9487d55B5eF50451e20ba2Edc8F4B7'

authorize.ts permalink

Khaos の Sign API がコールされたときに、正規の署名リクエストかどうかを判定して Promise<boolean | undefined> で返す関数を定義し、authorize でエクスポートします。

authorize の結果が true の場合にのみ Public Signature が生成され、Khaos サーバーに暗号化された秘匿情報が保存されます。

関数は引数として以下のようなオブジェクトを受け取ります。message は verify の対象となる文字列で、Twitter ID や GitHub リポジトリ名などが該当します。secret には秘匿情報となる文字列です。
request は @azure/functions の HttpRequest 型なので、Sign API がコールされたときの様々なコンテキストを利用できます。

type Options = {
readonly message: string
readonly secret: string
readonly request: HttpRequest
}

Khaos Starter Kit には HTTP ライブラリとして bent がインストールされているので、外部の API をコールして、message が正規のものかどうか判断することができます。また、関数型プログラミングライブラリとして ramda もインストールされているのでケースバイケースで利用してください。

import bent from 'bent'
import {always} from 'ramda'
import {FunctionAuthorizer} from '@devprotocol/khaos-core'

const fetcher = bent('https://api.foo.bar', 'json', 'POST')

export const authorize: FunctionAuthorizer = async ({
message: user_id,
secret,
request
}) => {
const authorization = `bearer ${token}`
const {headers} = request
const results = await fetcher<{verified: boolean}>(
'/verify',
{
user_id
},
{
Authorization: authorization,
'User-Agent': headers['User-Agent']
}
).catch(always(undefined))
return results?.verified
}

event.ts permalink

スマートコントラクトが emit するイベント名を Promice<string | undefined> で返す関数を定義し、event でエクスポートします。

関数は引数として以下のようなオブジェクトを受け取ります。

type Options = {
readonly network: 'mainnet' | 'ropsten'
}

Ethereum のメインネットまたは Ropsten テストネットによってイベント名を切り分けることができますが、多くの場合は同じイベント名を使用するはずです。

import {FunctionEvent} from '@devprotocol/khaos-core'
import {always} from 'ramda'

export const event: FunctionEvent = always(Promise.resolve('Query'))

oraclize.ts permalink

スマートコントラクトからのオラクルリクエストによってコールされる関数を定義します。この関数はとても重要なパートを担っています。この関数の返却値は後述の pack 関数によって整形された後にスマートコントラクトへのコールバックを介してブロックチェーンに渡されます。 oraclize 関数がコールされるのは、Khaos によってイベントが検出され、Public Signature をキーとして秘匿情報が取得されたあとです。

関数は引数として以下のようなオブジェクトを受け取ります。signatureOptions は、Public Signature を復号化したデータです。また、signatureOptionsauthorize 関数の結果が true を返した際に生成された Public Signature がイベントペイロードに含まれる場合にのみ定義されます。つまり、未認証の Public Signature が含まれている場合は undefined を返します。 query.publicSignature にはイベントペイロードに含まれる Public Signature、query.transanctionhash にはイベントを emit したトランザクションハッシュ、query.allData にはすべてのイベントペイロードが含まれています。

type Options = {
readonly signatureOptions?: {
readonly message: string
readonly id: string
readonly address: string
}
readonly query: {
readonly publicSignature?: string
readonly allData: Record<string, any>
readonly transactionhash: string
}
readonly network: 'mainnet' | 'ropsten'
}

関数の返却値は以下のようなオブジェクトで解決される Promise です。

type Options = {
message: string
status: number
statusMessage: string
}

次の例では、Public Signature の署名者とオラクルリクエスト送信者が同一アカウントであることを確認しています。

import {FunctionOraclizer} from '@devprotocol/khaos-core'

export const oraclize: FunctionOraclizer = async ({signatureOptions, query}) => {
const {queryId, fooId, account} = query.allData
const isSameId = fooId === signatureOptions?.message
const isSameUser = account === signatureOptions?.address
return isSameId && isSameUser
? {
message: queryId,
status: 200,
statusMessage: 'success'
}
: {
message: queryId,
status: 400,
statusMessage: 'fail'
}
}

pack.ts permalink

スマートコントラクトのコールバック関数名/引数により解決する Promise を返す関数を定義し、pack でエクスポートします。

関数は引数として以下のようなオブジェクトを受け取ります。results は oraclize が返す Promise が解決したときの値と同じデータです。

type Options = {
readonly results: {
readonly message: string
readonly status: number
readonly statusMessage: string
}
}

次の例では、 callback と名付けられた関数に [results.message, results.status, results.statusMessage] という引数でコールバックするように指定しています。

import {FunctionPack} from '@devprotocol/khaos-core'

export const pack: FunctionPack = async ({results}) => {
return {
name: 'callback',
args: [results.message, results.status, results.statusMessage]
}
}

テスト permalink

Khaos Starter Kit はデフォルトで ava でテストを書きます。テスティングフレームワークはあなたのプロジェクトに合わせて自由に変更できます。

テストは Khaos の利用には必須ではありませんが、仕様保証やメンテナンサビリティのために、なるべく正確なテストケースを用意することを強くお勧めします。

デプロイ permalink

すべてのインターフェイスとテストが完成したら、コードをデプロイします。

Khaos ではユーザー定義の関数は index.js の 1 ファイルにバンドルし、IPFS にデプロイします。Khaos Starter Kit ではデフォルトで Rollup によりバンドルし、Infura の IPFS ノードにデプロイします。

あなたが実行すべきコマンドはただこれだけです。

yarn deploy

ソースコードによっては Rollup の設定を更新する必要があったり、追加のプラグインをインストールする必要がある場合があります。その場合、あなたは rollup.config.js を書き換えたり、追加のプラグインをインストールできます。また、Rollup 以外のバンドラーを利用することもできます。

デプロイすると以下のような標準出力があるので、IPFS_HASH_FOR_FILE の値をメモしておいてください。

> {"Name":"index.js","Hash":"IPFS_HASH_FOR_FILE","Size":"871"}

Khaos Registry permalink

dev-protocol/khaos-registry: 🌌Khaos Registry for functions ipfs hash (github.com)

IPFS にデプロイされた関数のアドレスマップを管理しています。

このリポジトリをフォークして、map/functions.json に先ほどメモした IPFS_HASH_FOR_FILE の値を追記してください。

[
{
"id": "foo-bar",
"ipfs": "<IPFS_HASH_FOR_FILE>"
}
]

フォークリポジトリに変更を push したら、ソースリポジトリに対して Pull Request を作成してください。

現在は Khaos の利用用途を Dev Protocol 関連コントラクトに限定しているため、IPFS にデプロイされた関数の addresses が Dev Protocol 上のコントラクトであることを検証します。

将来的には、Khaos Registry はスマートコントラクトとして再構成、分散化される予定です。

Khaos Kit permalink

dev-protocol/khaos-kit-js: 🌌Khaos Kit for JavaScript (github.com)

Khaos Kit は、JavaScript(TypeScript) から Khaos とインタラクションするための API を提供します。

sign permalink

sign API は、Khaos の Sign API を呼び出す HTTP リクエストのショートハンドです。

この関数は 2 つの引数を取ります。最初の引数は Khaos の認証 ID で、Khaos Registry の id プロパティで指定したものと同じ文字列です。2 つ目の引数はネットワーク名として 'mainnet' または 'ropsten' を取ります。

// createPublicSignature.ts
import {sign} from '@devprotocol/khaos-kit'

export const createPublicSignature = sign('foo-bar', 'mainnet')

signKhaosSignOptions を引数に取る関数を返します。KhaosSignOptionsmessage は署名に用いたメッセージ、signature は Ethereum ウォレットで作成した署名、secret には秘匿化したい情報を渡します。

Ethereum ウォレットで署名するには、Web3 や Ethers の API を利用してください。

import {KhaosSignOptions} from '@devprotocol/khaos-kit'
import {createPublicSignature} from './createPublicSignature'

const getPublicSignature = async ({message, signature, secret}: KhaosSignOptions) => {
const results = await createPublicSignature({
message,
signature,
secret
})
console.log(results) // {publicSignature: 'eyJ...', address: '0x...'}
return results.publicSignature
}

emulate permalink

emulate API は、オラクルリクエストのためのイベントを emit したときの結果を off-chain でエミュレーションします。

この関数は 2 つの引数を取ります。最初の引数は Khaos の認証 ID で、Khaos Registry の id プロパティで指定したものと同じ文字列です。2 つ目の引数はネットワーク名として 'mainnet' または 'ropsten' を取ります。

// emulator.ts
import {emulate} from '@devprotocol/khaos-kit'

export const emulator = emulate('foo-bar', 'mainnet')

emulateKhaosEmulateOptions を引数に取る関数を返します。KhaosEmulateOptions@ethersproject/contracts の Event と同じ情報をすべてオプショナルにした event オブジェクトを取ります。 Event.argsArray{readonly [key: string]: any}extend した型ですが、KhaosEmulateOptions.argsRecord<string, string | number | undefined | null> でオーバーライドして単純化されています。

この関数の戻り値は、Khaos Starter Kit で作成した pack の戻り値に expectedTransaction を追加したデータです。

import {KhaosEmulateOptions} from '@devprotocol/khaos-kit'
import {emulator} from './emulator'

const predictCallbackStatus = async ({args}}: KhaosSignOptions) => {
const results= await emulator({
args,
})
console.log(results)
/**
* {
* data: {
* name: 'callback',
* args: ['0x...', 200, 'success'],
* expectedTransaction: {
* gasLimit: '122004',
* success: true
* }
* }
* }
*/

return results.data?.args[1]
}

🌈 この記事はお役に立ちましたか?

今後より良いコンテンツをお届けしていくために、ぜひご質問やフィードバックなどいただけると幸いです🌱
フォーラムはこちら

- Dev Protocol は全てOSSとして公開しています。ぜひIssueやPRを送ってください📢 時にバウンティがあります。
Dev ProtocolのGitHubはこちら

- Dev Protocol の改善提案(DIP)プロセスも公開されています。ぜひコメントをお待ちしています🌟
DIPはこちら