Chainlinkのノードを立ち上げて自作のExternal Adaptersを追加する ~オラクルノードの開始編~
Part 1で暗号資産の終値、Disparity(終値と移動平均の比率)、リターンの比率、単純移動平均などをChainlinkのスマートコントラクトから取得できるAPIを開発しました。Part 2ではExternal AdaptersというChainlinkノードへ機能追加できるAPIラッパーを検証できました。ここまできたら自らノードを立ち上げて、構築したExternal Adaptersをサービスインするしかありません。今回は前作成したAPIもより洗練させて多くの暗号資産を扱い、取得できる指標や時間足も増やして実用に耐え得るサービスを構築することを目指します。Chinlinkノードはメインネットへ構築します。すべてメインネットを使用していますのでコントラクトのデプロイやChainlink Marketの認証手数料など、暗号資産をそれなりに消費してしまいました。
目次です。長くなってしまったのでこの記事では上の3つ、Oracleコントラクトのテストまでを記載しました。
- Chainlinkノードの構築(VPS版)
- Oracleコントラクトのデプロイとジョブの追加
- デプロイしたOracleコントラクトのテスト
- Web APIとExternal Adaptersのデプロイ
- ノードへのExternal Adaptersの追加
- サービステストする!!
Chainlinkノードの構築(VPS版)
Chainlinkのソフトウェアはブロックチェーンのノードとは異なり、チェーンのコンセンサスへ参加することや台帳を保持していくことは行いません。あくまでオンチェーンとオフチェーンの間でデータを双方向に転送するミドルウェアとして動作するため大きなスペックは不要です。ただしEthereumネットワークとの連携が必要なのでGethやParityといったEthereumクライアントを動かしておく必要があると書かれています。つまりChainlinkノードはEthereumクライアントに加えてChainlinkのソフトウェアを動かすためのメモリおよびストレージが必要です。Chainlinkノードについての概要は、Chainlink Japanの『チェーンリンクノードオペレータとは何か』が分かりやすいです。[1]
ただEthereumクライアントを動かす代替として外部サービスのwss(Web Socket Service)を使用できます。この場合にChainlinkノードに必要なスペックは2Gのメモリと10Gのストレージからであると公式サイトに記載がありました。今回は非力な個人リソースで構築しますので、最安のプランでいきたいと思います。そのため2コア2G 100Gストレージで月800円のVPSサービスで構築したいと思います。Chainlinkノードを稼働させるには以下のようなコンポーネントが必要です。今回はPostgreSQLもローカルサーバ上へインストールします。
- Docker(Chainlinkノードクライアントのコンテナ)
- PostgreSQL
- Ethereumクライアント(Infuraを使用)
ここまで見て頂いてわかる通りChainlinkノードの構築の幅はかなり広いと思います。Dockerコンテナでソフトウェアが動くため、コンテナオーケストレーションを使用するという選択肢がまずは存在します。その場合に自らクラスタを立ち上げるのかクラウドサービスであるAKSやEKS、GKEを使うのかが検討されます。PostgreSQLについてクラウドであればマネージドサービスを使用できると思いますが、リモートエンドポイントでよいためここもどの場所にあるDBを使うか検討の余地が残されています。最後にEthereumクライアントをどこで持つかという選択肢があります。外部サービス(Chainstack、Fiews、Infura、LinkPool、QuikNode)を活用する場合にはスマートコントラクトとの連携の可用性や信頼性を自ノード側では一切コントロールできません。先日にInfuraで発生したような障害があると、なす術がないです。Infura 加油!!としか言えない。商用のレベルで構築する場合にはEthereumクライアントも自ら用意したほうが良いでしょうね。
GKEとCloud SQLを使用した例としてはこちらのブログが分かりやすかったです。ただ情報量が多くレベルも非常に高いですね。GCP、Kubernetesなどハイブリッドな知識に必要です。[2]
というわけで繰り返しですが、ここでは最小スペックで外部サービスであるInfuraを使いつつ1台のサーバのコンテナとローカルのPostgreSQLでChainlinkノードを構築します。is 簡単ですね。こちらの公式の『Running a Chainlink Node』に沿って構築しました。[3]
2コア2G、ストレージ100GBのVPSサーバでやっています。サーバの選択でUbuntu/CentOS/Debianなどが選べましたがあまり使ったことのないCentOSにしてみました。まずはDockerをインストールします。 バージョン20.10.0がインストールされました。ChainlinkクライアントのDockerイメージはこちらから確認できます。 0.9.4
をプルしておきます。これは後で使います。※最新版は 0.9.6
ですがこのイメージの実行でエラーが発生した上にいくつかバグが報告されているようです
$ sudo yum update$ curl -sSL https://get.docker.com/ | sh
$ sudo systemctl start docker
$ sudo usermod -aG docker $USER
$ docker versionClient: Docker Engine - Community
Version: 20.10.0
API version: 1.41
Go version: go1.13.15
Git commit: 7287ab3
Built: Tue Dec 8 18:59:27 2020
OS/Arch: linux/amd64
Context: default
Experimental: true
...$ docker pull smartcontract/chainlink:0.9.6
次はPostgreSQLのインストールです。PostgreSQLのRepo RPMSからリポジトリを追加して最新のPostgreSQLサーバのソフトウェアをインストールしました。REPO RPMSのサイトはこちらです。[4]
PostgreSQL 13 for RHEL/CentOS 8 — x86_64 が現在の最新版のようです。CentOS 8ではdnf(Dandified Yum)でpostgresqlを無効にしてからでないとインストールできません。dnfはyumの後継でCentOS 7や8で使用できるようになっているとのこと。
$ sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm$ yum module list postgresql
PostgreSQL 13 for RHEL/CentOS 8 - x86_64 87 kB/s | 193 kB 00:02
...$ sudo dnf -y module disable postgresql
$ sudo yum -y install postgresql13-server
Installed:
postgresql13-13.1-1PGDG.rhel8.x86_64
postgresql13-libs-13.1-1PGDG.rhel8.x86_64
postgresql13-server-13.1-1PGDG.rhel8.x86_64Complete!
引き続きでPostgreSQLの設定と起動を行っていきましょう。 posgres
ユーザでデータベースクラスタを作成しPostgreSQLを起動します。またOS起動時に起動するようにUpstartの起動登録を行います。データベースクラスタを作成した直後では postgres
template0
template1
の3つのデータベースがデフォルトで存在しています。
$ su - postgres -c '/usr/pgsql-13/bin/initdb -E UTF8 --locale=C -A scram-sha-256 -W'Success. You can now start the database server using:
/usr/pgsql-13/bin/pg_ctl -D /var/lib/pgsql/13/data -l logfile start$ sudo systemctl start postgresql-13
$ sudo systemctl status postgresql-13
$ sudo systemctl enable postgresql-13
$ su - postgres
$ psql -l
新しいロール chainlink
および新しいデータベース chainlink
を作成してロールへ権限を付与します。ここまででChainlinkノードの設定に入る前の準備が完了です。次からはChainlinkノード自体の設定ファイルへ移ります。
公式ではRinkebyやKovanの設定方法もあるのですが今回はメインネットのみ設定および稼働させていきます。サービス自体を本番環境で提供したいためです。設定ファイルを以下のように作成しました。パスワードなどの情報を環境変数に埋め込んでいますが、できればVaultのような機密情報を管理するサービスを利用しましょう。本番稼働する際にこの点はまた検討したいと考えています。クラウドサービス(AWS/Azure)を利用されている方はAWS Secret ManagerやAzure Key Vaultがマネージドサービスとして提供されています。Hashicorp Vaultなどでもいいと思います。
$ mkdir ~/.chainlink
$ echo "ROOT=/chainlink LOG_LEVEL=debug ETH_CHAIN_ID=1 CHAINLINK_TLS_PORT=0 SECURE_COOKIES=false GAS_UPDATER_ENABLED=true ALLOW_ORIGINS=*" > ~/.chainlink/.env
$ echo "ETH_URL=wss://mainnet.infura.io/ws/v3/<your project ID>" >> ~/.chainlink/.env
$ echo "DATABASE_URL=postgresql://$USERNAME:$PASSWORD@$SERVER:$PORT/$DATABASE" >> ~/.chainlink/.env
$ cd ~/.chainlink && docker run -p 6688:6688 --name chainlink --network="host" -v ~/.chainlink:/chainlink -it --env-file=.env smartcontract/chainlink:0.9.4 local n
firewall-cmd
の設定です。Web UIへのアクセスにTCPポートの6688を使用するようなので事前に開放しておきます。UDPはたぶん不要です。またローカルのホスト上のデータベースを使用する場合にはdocker runのオプションで --network="host"
を付与する必要があります。コンテナ内部からサーバローカルのPostgreSQLへ到達するにはコンテナ内に書いたlocalhostで接続できないためです。
で、ノード立ち上げ時にエラー発生です。PostgreSQLがSSL接続できないためにGoのプログラムで怒られています。対応は簡単でサーバ証明書を作成して、PostgreSQLの設定へ反映しPostgreSQL上でSSLを有効化するだけです。自己証明書など必要なファイルを作成しましょう。
とりあえず鍵を作り、CSRを作り、そこから自己署名したサーバ証明書を作成します。作成したファイルを元にPostgreSQLの設定を変更しましょう。設定変更が必要なファイルは /var/lib/pgsql/13/data/postgresql.conf
および /var/lib/pgsql/13/data/pg_hba.conf
です。 pg_hba.conf
ではタイプをhostsslに限定することでSSL通信のみのパスワード認証に限定します。
$ cd /etc/pki/tls/certs/
$ openssl genrsa -out server.key 2048
$ openssl rsa -in server.key -text -noout # confirm
$ chmod 400 server.key
$ chown postgres.postgres server.key
$ openssl req -new -key server.key -out server.csr # create a CSR
$ openssl req -in server.csr -text -noout
$ openssl x509 -req -in server.csr -signkey server.key -sha256 -days 3650 -out server.crt
$ cp server.crt root.crt # copy as a root certificate
PostgreSQLを再起動して再びChainlinkコンテナイメージを立ち上げるコマンドを入力してください。Chainlinkのノードが立ち上げり色々なログが出力されると思います。ノードのプロンプトで2つ設定しなければならない項目が出てきます。以下のメールアドレスやパスワードを画面の指示に沿って入力してください。Chainlinkのノードが立ち上がるとブラウザからポート6688でWeb UIへアクセスが可能になります。
- キーストアファイルをアンロックするためのウォレットのパスワード
- APIのメールアドレスとパスワード(Web UIのログイン用)
$ sudo systemctl restart postgresql-13
$ cd ~/.chainlink && docker run -p 6688:6688 --name chainlink --network="host" -v ~/.chainlink:/chainlink -it --env-file=.env smartcontract/chainlink:0.9.4 local n
Chainlinkノードのイメージは現在 0.9.6
が最新ですがこのイメージでノードがうまく動作しませんでした。 0.9.4
で問題なく動作しています。Web UIへアクセスすると以下のようなオペレータ管理画面が確認できます。Chainlink CLIでノードを立ち上げる際に -p
オプションでファイルからパスワードを、 -a
オプションでファイルからAPIのメールアドレスとパスワードを渡すことができます。[5]
ステータスの確認を行います。特に問題がなければChainlinkノードの構築は完了です。次はOracleコントラクトのデプロイとジョブの追加へと移ります。以下のDockerコマンドは名前付きコンテナとして、アカウントのパスワードファイル、Web UIのメールアドレス・パスワードをオプションで渡す例です。
$ cd .chainlink
$ docker run -p 6688:6688 --name chainlink -d --network="host" -v ~/.chainlink:/chainlink -it --env-file=.env smartcontract/chainlink:0.9.4 local n -p /chainlink/.password -a /chainlink/.api$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
48d03f66860b smartcontract/chainlink:0.9.4 "chainlink local n -…" 3 minutes ago Up 3 minutes chainlink$ docker logs chainlink
...
Oracleコントラクトのデプロイとジョブの追加
Chainlinkノードの物理的構築は完了しましたが、オラクルとして機能させるためにはOracleコントラクトのデプロイ、ノードの追加およびジョブの追加の作業が必要です。ノードオペレータが所有するOracleコントラクトに対して、クライアントはリクエストを送信します。Part 1でテストしたように対象のノードのOracleコントラクトのアドレスとサポートするジョブを記述してスマートコントラクトから利用できます。この章は以下のアーキテクチャ図の『Oracle Contract』をデプロイする作業です。
以下の公式ドキュメント参考にデプロイしました。[6]
Oracleコントラクト自体は既にgithubに用意されているのでRemix上からぽちぽちとクリックするだけで簡単にデプロイできます。接続しているネットワーク、ウォレットを間違えないようにしてください。メインネットへデプロイしたコントラクトのトランザクションです。トランザクションからコントラクトのアドレスを調べて控えておきます。コントラクトのアドレスは後の設定で使用します。
次はノードのEthアドレスをOracleコントラクトへ登録します。 setFullfillmentPermission
という関数があるので “<chainlink node address>”,true という文字列を入力してトランザクションを投げてください。関数名である setFullfillmentPermission
というオレンジのボタンを押すとトランザクションを作成できます。Chainlinkノードのアカウントアドレスが分からない場合はWeb UIでログインして『Configuration』ページから確認ができます。このトランザクションでOracleコントラクトにChainlinkノードのEthアドレスとのやり取りを許可できます。
デプロイしたOracleコントラクトのテスト
最後にスマートコントラクトから構築したChainlinkノードとデプロイしたOracleコントラクト経由でEthの価格が取得できるかを確認します。今回メインネットでテストしてますのでデプロイしたコントラクトへリアルのLINKを送付する必要がありました。LINKの送金はBinanceのwithdrawでMetaMaskへ送り、そこからデプロイしたコントラクト宛に送っています。またChainlinkノードのEthアドレスへのEth送金も必要です。この理由については後ほど説明します。以下は構築したChainlinkノードとデプロイしたOracleコントラクトを実際のスマートコントラクトで使用するステップの箇条書きです。
- クライアントコントラクトであるConsumer.solをデプロイする
- LINKトークンをコントラクトアドレスへ送金する
- ChainlinkノードのアドレスへEthを送金する
- requestEthereumPriceを実行する(ジョブを作成していること)
- currentPriceを実行して現在のEth価格を確認する
上のリストの中でいくつかハマったポイントを紹介していきます。Consumer.solは公式サイトのTestConsumer.solをそのままデプロイしました。こちらがデプロイしたコントラクトです。後から気がつきましたがここではrequetEthereumPriceだけ確認できればよいのでコードは最小限に留めたほうがデプロイコストは安くすみます。
このデプロイコントラクトではサービス料支払いが1 LINKに設定されています。LINKは後から自アカウントへ引き出すことができるもののトークンの引き出しにはトランザクション手数料もかかるので0.1 LINKへ変更しても良いかと思います。1 LINKはいま10ドル以上なので複数回リクエストを送るとすぐに数十ドルに達してしまいます。
// uint256 constant private ORACLE_PAYMENT = 1 * LINK;
uint256 constant private ORACLE_PAYMENT = 0.1 * LINK;
requestEtherumPriceを呼び出した際に、Chainlinkノード上のジョブが『Pending Outgoing Confirmations』でスタックしました。投稿でこのスタックの原因は3つ考えられると記述がありますが、今回の例では最初のChainlinkノード上のEthが不足しているが理由でした。ChainlinkノードのEthアカウントはトランザクションをOracleコントラクトへ送るためにEthを消費します。そのためEthが足りない場合にジョブを完了できません。
『Pending Outgoing Confirmations』となっても一旦Ethが着金されるとすべてのコンファメーションがセトルされますので安心してください。スタックしているコンファメーションに対応する必要はありません。Web UIのジョブからCompletedとなりワークフローが完了していることが確認できるようになります。
requestEthereumPrice関数へはOracleコントラクトのアドレスとジョブIDを渡します。 “<oracle contract address>”, “<EthUint 256 Job ID>”という文字列を入力して requestEthereumPrice
というオレンジのボックスをクリックしてトランザクションを作成します。 currentPrice
をクリックするとオラクルから帰ってきた値が参照できます。
スマートコントラクトから手数料として支払われたLINKはOracleコントラクト上に蓄積されています。このLINKはOracleコントラクトの所有者(この場合はノードオペレーター)だけ引き出すことが可能です。Oracleコントラクトの withdraw
関数でLINKを引き出してください。ノードのオペレーションを開始したらChainlink Marketへノードを追加しましょう。Sign-upするとノードが追加できます。メインネットでのノードの認証には$32程度のコストがLINK払いでかかります。
というわけでChainlinkノード屋はじめました。[8]