Contract Application Binary Interface(ABI)とは
今回はEthereumでDappsを開発した経験のある人は1度は目にしたことがある、「ABI」について解説します。
ABIの仕様は以下のSolidityのドキュメントにまとめられています。
solidity.readthedocs.io
ABI仕様の技術的な説明は上記ドキュメントに譲るとして、本記事では、ABIの目的やABIがどのように参照・利用されているか?に焦点を当てて解説します。
ABI(Application Binary Interface)とは
ABIとは一般的な用語であり、バイナリーファイル(主に実行形式ファイルと呼ばれるもの)へのアクセスに対して互換性を与えるために定義されるものです。Linux系ではExecutable and Linkable Format(ELF)などがあります。詳しくは下記のwikiなどを参照ください。
ja.wikipedia.org
とりわけ、EthereumではEVM上で実行可能なバイナリコードであるコントラクトと、clientツールであるweb3.jsなどの各種ツールがTransactionを介してやり取りを行う方法について互換性を与えるために定義されています。
目的
SolidityのドキュメントのBasic Designのセクションに書いてあるとおり、ABIはコントラクトの機能をEthereumの外部、もしくは別のコントラクトから呼び出すための標準化された方法を定義しています。
コンパイラーがABIの仕様に準拠したbytecodes(=opecodes)を生成し、また、web3.jsやether.jsなどのclientツールがABIに準拠したデータ構造で呼び出しを行うことで、コントラクトの任意の機能にアクセスすることが出来るようにするためのものです。
ABIに準拠した言語(コンパイラ)
コントラクト記述言語としても有名なこれら2つの言語はABI仕様に準拠しています。つまり、SolidityとVyperで記述され、コンパイルされたコントラクトはweb3.jsからはその言語の違いを意識せずに呼び出すことが可能です。これは両方の言語でコンパイル時に生成するbytecodes(=opecodes)自体は違っていますが、ABIに準拠した構造を持つためです。
すでにメンテナンスされていませんが、過去にはserpentといった、ABIに非準拠なコントラクト記述言語もありました。serpentでは関数という概念はないため、web3.jsからは呼び出すことができず、コントラクトの内部構造を理解している開発者が直接Transactionに適切なデータを付与して呼び出す必要がありました。
基本設計
ABIの基本設計として、生成されたデータおよび構造にたいして自己定義情報(self-described)を持ちません。つまりコントラクトの関数を呼び出すために必要な引数の情報や、コントラクトから受け取ったデータをデコードするための型情報は外部のスキーマ定義ファイルから得る必要があります。スキーマ定義ファイルについては後述するJSONフォーマットの項を参照ください。
また、ABIは強い静的型付な定義であり、動的な関数はサポートしておりません。つまり、ABIではコンパイル時には静的関数のみを生成するため、コントラクトの状態によって関数の数が増えたり、定義していない任意の引数を受け取る関数といったものは定義できません。fallback関数についてはSolidityの拡張定義のような気もするので、ABI仕様なのかどうかはグレーゾーンです。
関数セレクタ
コントラクトが外部に公開する関数を呼び出すための仕様を定義したものになります。公開された関数はそれぞれ一意のIDが振られます。ABI仕様としては関数のIDの生成方法が定義されています。これらの関数セレクタの仕様に準拠することで、web3.jsからコントラクトの任意の関数へのアクセスが可能となっています。
コンパイラーが準拠すべき処理
コンパイラーは関数本体の処理をを行うbytecodesを生成するのはもちろんとして、呼び出しデータの先頭4byteをfunctionIdとして解釈し、関数本体にJumpするbytecodesも生成しなければいけません。
clientツールが準拠すべき処理
clientツールでは、呼び出したい関数に対して仕様に沿ったfunctionIdの生成を行い、そのIDをtransactionのdata部の先頭4byteに配置するという処理を行います。
より詳細な説明は過去の記事でまとめていますのでそちらを参照ください。
y-nakajo.hatenablog.com
引数のエンコーディング
関数に渡す引数のエンコーディング方法(主にデータフォーマットについて)も仕様として定義されています。要素数が不定である動的変数(uint[]やstringなど)が含まれていた場合のデータのエンコード/デコードのための仕様がメインとなります。個別の型に対するバイナリエンコーディングについては型定義のセクションでさらに細かく定義されています。
型定義(エンコーディング)
それぞれの型に対してのエンコーディング方法を定義しています。コントラクトでstringやuintといった型が扱えるのは、コンパイル時にここで定義されたエンコーディング方法でデータをエンコードするための処理が埋め込まれるためです。(例えばaddress型を定義した場合は、渡された32byteのデータから上位12byteを0にリセットするといった処理)。
ちなみに、Solidityではfixed型をまだサポートしていませんが、VyperではサポートしているためVyperのみ小数が扱えます。
イベント
ethereumのLogを利用したイベント機能についての仕様が定義されています。イベントの識別子の生成方法、及び、indexedをつけたパラメータをtopicとして登録する方法、最後に残りのパラメータをdataフィールドに格納する方法について定義しています。
clientツールが準拠すべき処理
web3.jsなどのclientツールでは、この仕様を用いて監視するイベントを絞り込むtopicの生成、及びイベントで記録されたデータのデコードを行います。
JSONフォーマット
Web3.jsなどのclientツールが、コントラクトとやり取りするためのインターフェーススキーマをJSONフォーマットのデータとして定義するための仕様です。JSONデータには、constructor, public function, Eventのシグネチャーが定義されています。clientツールではこれらの情報をパースすることで、コントラクトの関数を呼び出す時に必要となる、functionIDの生成と、引数データのエンコーディングなどを決定します。また、イベントで通知されたパラメータや、関数を呼び出した時の戻り値の解釈のためにも、このJSONデータを参考にします。
コンパイラーが準拠すべき処理
コンパイラーはソースコードをコンパイルする時にソースコードから型情報を解析して、適切なエンコードを行うbytecodes(=opecodes)を生成します。その時に同時にスキーマ情報を定義したJSONデータを、この仕様のフォーマットに従って生成します。
clientツールが準拠すべき処理
web3.jsなどのclientツールでは、このJSONデータを解釈する事で、コントラクトと適切なデータ形式でやり取りを行うためにエンコード/デコード情報を取得します。そのため、基本的にはJSONデータがないとclientツールは任意のコントラクトとやり取りを行う事ができません。
前述した通り、コンパイラーが生成するbytecodesは処理それ自体のみであり、bytecodesの中には変数や引数の型情報、また関数が要求する引数の情報などは一切含まれていません。
そのため、web3.jsなどのclientツールからコントラクトにアクセスするためには、bytecodesには含まれていない、これらの変数の型情報および、関数の定義情報を持つJSONデータが必要になります。