Chainlinkでデータを公開するまでの道のり ~Part1 Web APIを作る編~
昨年の10月にChainlinkを使用してスマートコントラクトから仮想通貨のレートを取得するという内容の記事で、分散型のオラクルとして最も有名であると思われるChainlinkとConsensysの提供するSaaSサービスであるKaleidoをご紹介しました。今回は備忘録としてオラクルやChainlinkの復習および作成したWeb APIとExternal Adaptersを使用したスマートコントラクトからのデータ取得をやってみます。ChainlinkはLINKトークンが時価総額上位(現在、7位ぐらい)に入るなどして注目を集めましたが、オラクルとしてサービスを使用している企業やサービス提供を行っているユーザはまだ少なそうです。記事は2つに分けて前半でChainlinkのノードからデータをリクエストするためのWeb APIの作成とデータを取得するスマートコントラクトの作成、後半でExternal Adapterが必要なケースの説明とオラクルノードへのホスティング方法を見てみたいと思います。システムの基盤はAWS SAM(Serverless Application Model)を使っていきます。
前回の記事はChainlinkやオラクルの基礎的な部分についてカバーしているのでご興味のある方は過去記事をご覧ください。[1]
この記事は2つの連続した記事の内の前半で以下の内容をカバーしています。
- オラクル(Oracle)とは
- 分散型オラクル『Chainlink』
- AWS SAMでWeb APIを作る
- スマートコントラクトからWeb APIを呼ぶ
オラクル(Oracle)とは
『Code is law』はスマートコントラクトを信じる人たちの合言葉です。コード化された世界では中間者や人が介在することなく、自動的に様々な処理がコードによって執行されていきます。透明性、非中央集権などブロックチェーンの推進する力は強力ですが、そのナラティブも現実的な世界との意味の連関なくしては成立させることができません。例えば、婚姻関係をスマートコントラクトに書き込むことを考えてみましょう(既にありますよね)。それはデータとしてオンチェーン上に存在しますが、婚姻関係の効力が認められるためには、法律的に国家・行政とのデータ連携が必要となります(国家がシステムとして存在する前提ですが)。
様々な現実世界での規約やオフチェーンのデータ、例えば為替・金利など、とスマートコントラクトの処理を結びつけるためには、スマートコントラクトと現実世界を繋ぐ出入口(ゲートウェイ)が必要になります。そのゲートウェイを一般的にオラクル(Oracle)と呼んでいます。オラクルとは預言や信託を本来は意味します。スマートコントラクトと外部の情報の連携を行うことで、法的な効力や資産(アセット)および実データなどの情報をスマートコントラクトの世界に関連させることができるようになっています。[2]
別の例としてデリバティブの金利スワップ契約を考えてみます。オラクルから利率に関する外部データをフィードし、エスクローコントラクトを通じて当事者間の金利スワップを自動的に処理するスマートコントラクトが考えられます。金利スワップの交換は基本的にOTC(店頭取引)であるため、パブリックブロックチェーン上にエスクローコントラクトを展開しておくことで、中間者を排除し、契約コストや執行コストといった信用コストを大幅に削減できる可能性があります。この例でもオラクルという外部サービスによってこのエスクローコントラクトが成立していることが分かります。
分散型オラクル『Chainlink』
Chainlinkは分散型ネットワークで主に外部のデータソースへの接続を提供するオラクルサービスです。入力として外部APIと連携してデータを取得することや(オフチェーンからオンチェーン)、外部のクレジットカードネットワークを通じた支払いの処理(オンチェンからオフチェーン)などを行うことができます。さらに分散型のノードでネットワークが構築されていることで単一障害点の問題を回避しています。Chainlinkは基礎としてチュートリアルに3つのサンプルが紹介されています。[3]
例として3番目の外部APIを呼び出してデータを取得するサンプルを動かしてみました。このサンプルはCryptoCompareのEthの取引ボリュームを取得するAPIを叩くSolidityのコントラクトです。SolidityのコードはChainlink.Clinetを継承してコーディングする必要があり、このサンプルのコントラクトでは requestVolumeData()
というChainlinkネットワークへデータ取得をリクエストする関数とコールバック関数が主に定義されています。コントラクトはKovanネットワーク用に書かれているため、KethとKovanネットワークのLINKをChainlink Kovan Faucetから取得しておく必要があります(Kethはコントラクトのネットワークへのデプロイに必要です、ちなみにKethはKovan Ethのこと、ややこしいw)。[4]
ここをクリックするとREMIX上にコントラクトが展開されます。まずはコントラクトをコンパイルしましょう。”Compile APIConsumer.sol”をクリックしてコンパイルの完了を待ちます。Truffleなどの開発環境がある場合には、REMIXではなくローカルでコーディングとコンパイルを行っても同じ結果が得られます。
次にKovanネットワークへデプロイします。今回デプロイしたコントラクトのトランザクションハッシュはこちらです。Kethがないとデプロイできないため、事前にFaucetから入手しておきましょう(githubのアカウントが必要でした)
あとはデプロイされたコントラクト上で requestVolumeData
を呼びリクエストを行うと volume
変数に取得すべきEthの取引ボリュームが格納されているはずです。プチハマりポイントですがChainlink Kovan Faucetから送付するLINKはデプロイしたコントラクトのアドレスを宛先とする必要があります。なぜならオラクルサービスを利用する主体はデプロイしたスマートコントラクトなので手数料はこのコントラクトから支払うからです。[5]
以下の画像のように volume
にEthの取引高の情報が格納されていることが確認できました。数字の桁が非常に大きいですが、これはAPIから取得した値の小数点を打ち消すために関数内で取得した値を10の18乗しているからです。この処理はaddIntアダプターで行われています。
基本的にこの方法で外部のAPIをコールし、どのような値や情報でも取得することができると書かれています。ただAPIエンドポイントを含むスマートコントラクトを作成しなくても既にオラクルノードにサポートされているAPIエンドポイント呼び出しを利用する仕組みが存在します。オラクルノードが提供するサービスを使用してデータ取得できる仕組みがオラクルジョブ(Oracle job)です。分散化されたネットワークに展開されるオラクルノードはデータソースやネイティブトークンであるLINKの手数料価格によって差別化を図っています。各ノードは稼働時間や応答までの平均時間、完了したジョブの成功率など、様々な指標によって評価されており、評価の高いノードは利用者や開発者にとって自然に選択されやすくなります。
オラクルジョブとして紹介されているのがKovanネットワークでCoingeckoのETH/USDを取得する例です。オラクルへのリクエストとコールバック関数が非常に簡素に記述できるためスマートコントラクトが分かりやすくなっています。このジョブを使用すればスマートコントラクトをURLごとに書き換えてデプロイ、といったことを行わなくてよくなります。[6]
既存のオラクルノードやジョブはChainlink Marketから簡単に検索して見つけられます。この例はAlpha Chain のオラクルアドレス『0xAA1DC356dc4B18f30C347798FD5379F3D77ABC5b』上のジョブID『9cc0c77e8e6e4f348ef5ba03c636f1f7』で利用できると判断できます。この対象オラクルノードのアドレスとジョブIDをスマートコントラクトで指定することでオラクルジョブが実行できます。[7]
AWS SAMでWeb APIを作る
まずはWeb APIを自作します。最近DeFiのデータから計算した指標をグラフ表示するTwitterアプリを作成しました。その一部のコードを流用してAPI Gateway/AWS Lambda/Dynamo DBで各トークンやコインのデータから計算可能な統計値やテクニカル指標を返すWeb APIの作成を目指します。以下のようなデプロイを行っていきました。
まずDynamoDBだけTerraformでデプロイしました。今回必要なテーブルは1つのみでそのテーブルは1回作成したら削除する必要はありません。SAM側のアプリは変更に伴い何度も削除やデプロイを行う可能性があるのでDynamoDBだけデプロイの単位を分けました。SAMのアプリケーションも2つに分けてしまいましたがSAMは1つの単位でも問題ありません。
デプロイの順番。それぞれ簡単に見ていきます。コードはgithub上にあります。
- TerraformでDynamoDBをデプロイする
- AWS LambdaとCloudwatch EventsをAWS SAMでデプロイする
- API GatewayとAWS LambdaをAWS SAMでデプロイする
DynamoDB
パーティションキーは文字列 name
で暗号通貨の名前を格納することとします。例えばbitcoinやethereumなど。レンジキー、その他のアトリビュートは指定していません。アトリビュートは想定として終値やDisparity(終値と移動平均の比率)、リターンの比率などとしています。以下のTerraformコードでサクっとテーブルをデプロイしました。
$ terraform version
Terraform v0.12.6$ cd main
$ terraform init
$ terraform plan # confirm
$ terraform apply # run
aws dynamodb list-tables
で指定した名前のテーブルができていることを確認します。今回テーブル名はchainlinkとしていますが環境内で重複していなければ何でも良いです。
$ aws dynamodb list-tables
{
"TableNames": [
"chainlink"
]
}
Ingest App SAM
SAMでデプロイするLambda関数を作成しましょう。最初のSAMはCloudWatch EventsでLambda関数をトリガーし、Lambda関数でCoinGeckoのAPIを叩き取得したOHLCデータから計算情報をDynamoDBへ書き込むアプリケーションです。CoinGeckoからOHLCデータを取得する部分とDynamoDBにレコードを新規追加するコードは別クラスとしました。 scipy
および numpy
はAWSの提供するレイヤーで提供し、 pandas
は自作したレイヤーで読み込ませます。 pandas
を含むレイヤーの作成方法は別記事でまとめています。このテストコードでは {"operation": "test"}
をLambda関数の event
へ渡すとCoinGeckoから指定した日数のOHLCデータを取得して、そこから終値、Disparity(終値と移動平均の比率)、リターンの比率、単純移動平均を計算してテーブルへ格納するようになっています。
次にSAM用のtemplate.yamlファイルを書きます。30分毎にLambda関数を呼び出して値をDynamoDBに追加するようなイベントを設定します。上記のLambda関数がCoinGeckoから1日分、30分足のOHLCデータを取得する状態なのでCloudwatch Eventsの設定も30分毎(毎時15分と45分)にトリガーするようにしました。sam cliを使うとレイヤー込みでローカルテストしてアプリケーションの挙動を確認できます。 SAMではレイヤーを含むコンテナは samcli/lambda
としてラップして立ち上げてその中でコードを実行してくれるようです。
このコードが動くかをテストしました。chainlinkというDynamoDBのテーブルにアイテムが無事追加されていることが確認できます。パーティションキー name
がbitcoinのアイテムで、 Close
dis
return
sma
が存在します。それぞれ終値、Disparity(終値と移動平均の比率)、リターンの比率、単純移動平均に対応します。最終的にはこれらの値がChainlinkのノードのオラクルジョブ経由で取得できるようになるはずです。
$ sam local invoke IngestApplication --event event.json --region ap-northeast-1 --env-vars vars.json# compile and deploy
$ sam package --template-file template.yaml --s3-bucket <s3 bucket> --output-template-file compiled.yaml
$ sam deploy --template-file compiled.yaml --stack-name <stack name> --capabilities CAPABILITY_IAM --parameter-overrides TableName=chainlink
API Gateway SAM
お次はAPI GatewayとDynamoDBから値を引っ張ってくるLambda関数をデプロイするSAMに移ります。SAMを使うと sam local
によってAPI Gatewayもローカル環境でテストすることができます。このローカル実行で使用されるDockerイメージは lambci/lambda
となっています。SAMの場合API GatewayはリソースタイプAWS::Serverless::Function配下のイベントとして定義できますが、この方法で作成したAPI GatewayはLambdaプロキシ統合となっているようです。ですので作成するLambda関数もプロキシ統合に対応する形で書く必要がありました。[7]
こちらが使用したtemplate.yamlです。APIのパスは /test
でkeyというクエリストリングを付けて呼ぶことができるようにします。例えば http://x.x.x.x/test?key=bitcoinでビットコインの情報を取得できます。Ethであればhttp://x.x.x.x/test?key=ethereumです(これは今回作っていませんが)。Lambdaプロキシ統合の場合はクエリストリングが event['queryStringParameters']
に保持されています。
API GatewayをローカルでテストするにはSAMをビルドする必要がありました。 LambdaFunctionAPIGateway
というリソースをビルドします。 .aws-sam/build
配下にアーティファクトとテンプレートがビルドされています。これでテストの準備は完了です。
$ sam build
Building resource 'LambdaFunctionAPIGateway'
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySourceBuild SucceededBuilt Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yamlCommands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
sam local start-api
コマンドでAPI Gatewayをローカル稼働させます。 vars.json
でLambda関数で使用する環境変数を渡せます。このサンプルではテーブル名であるchainlinkが必要です。ここらへんの操作は sam local invoke
と同じようなオプションですね。あとはローカルでこのAPIを呼び出すのみです。パス /test
へクエリストリングとして ?key=bitcoin
を渡しchainlinkのテーブル値がローカルのWeb APIで正しく取得できていることを確認しました。
$ sam local start-api -p 5000 -n vars.json
2020-12-04 00:00:09 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)$ curl -X GET -H "Content-Type: application/json" http://127.0.0.1:5000/test?key=bitcoin{"Close": 19313.68, "sma": 19311.429999999993, "dis": 1.0001165113096238, "name": "bitcoin", "return": 0.004552131054771147}
AWS上へデプロイしてみます。SAMなのでデプロイ方法は同じです。API GatewayはProdとStageという2つのステージがデプロイされました。ここではステージによって挙動を変更していないのでWeb APIとしての動きは両方のステージで同じです。ステージ情報はプロキシ統合の場合 event['requestContext']
に格納されているのでその値を活用すれば環境別でより柔軟なコントロールが可能かと思います。[8]
# compile and deploy
$ sam package --template-file template.yaml --s3-bucket <s3 bucket> --output-template-file compiled.yaml
$ sam deploy --template-file compiled.yaml --stack-name <stack name> --capabilities CAPABILITY_NAMED_IAM --parameter-overrides TableName=chainlink
最後にデプロイしたAPI Gatewayのエンドポイントを呼び出して値が取得できるかを確認しておきます。ここではProdの方を使用しました。次の節のスマートコントラクトの作成でもProd環境の方のURLを使います。
$ curl -X GET -H "Content-Type: application/json" https://<your serverless api>.execute-api.ap-northeast-1.amazonaws.com/Prod/test?key=bitcoin{"Close": 18736.36, "sma": 18728.135, "dis": 1.0004391788077138, "name": "bitcoin", "return": 0.0013430437229406955}
スマートコントラクトからWeb APIを呼ぶ
実験の最後はChainlink.Clinetを継承してデプロイしたスマートコントラクトから自作したWeb APIの値を取得する、です。SolidityのスマートコントラクトをChainlinkのサンプルから自作したAPIの仕様に沿って変更する必要があります。変更箇所は2つです。
- 呼び出しURLをCryptoCompareから自作Web APIのものへ変更
- Pathアダプターを使用したjson解析の指定を変更
こちらが修正したAPIConsumer.solです。このスマートコントラクトではChainlinkのノードのジョブ『29fa9aa13bf1468788b7cc4a500a45b8』を引き続き使用して自作のWeb APIから値を取得しています。取得する値は return
でこのパラメータの実体はDynamoDBのテーブルのchainlinkに保存されているリターンの比率です。Lambda関数で設定している window
の値によりますが、いまはwindow設定が4でこれは2時間に相当します。2時間前の終値に対する現在の終値の割合が分かります。
Kovanネットワークへコントラクトをデプロイしてテストしました。手順としてはKethを入手、コントラクトをデプロイ、Chainlink Kovan FaucetからスマートコントラクトのアドレスへLINKを送付、requestVolumeDataを発火の流れです。デプロイしたコントラクトはこちらです。Kovanはトランザクションやデプロイが早くていいですね。
volume
の値を参照すると1343043722940696が取得できました。実際の return
の値は0.0013430437229406955ですがaddIntアダプターで数値を10の18乗しているため小数点の位置が異なります。最後の桁が四捨五入されてしまってますが、値は一致しています。無事スマートコントラクトから外部のリアルタイムデータを参照できました。
今回のケースではChainlinkのオラクルノードとジョブIDによって簡単に外部APIからデータ取得が行えることを確認できました。HttpGetやJsonparthを行う操作はコアアダプターと呼ばれておりChainlinkノードネイティブです。External Adaptersはオラクルノードに追加できるオープンソースパッケージのようなもので他のブロックチェーンとの連携など多様なカスタムを可能とします。例えばWeb APIの認証なんかもExternal Adaptersを追加することで可能だとのこと。次回はこのExternal Adaptersについて技術的探索をしてみたいと思います。今回作成したコードはこちらに置いておきます。