株式会社 proof ninja 技術顧問の池渕です.
proof ninja では,関数型プログラミング言語 OCaml で Ethereum 上のスマートコントラクトを書けるようにするため,OCaml コードを Ethereum VM (EVM) のバイトコードへ変換するコンパイラ ocaml2evm を開発中です.
まだまだ開発途上ではありますが,この記事では ocaml2evm の現在の状況・使い方について紹介します.
ocaml2evm のソースコード,環境準備
ソースコードは以下:
ビルド等のために必要なもの:
- OCaml バージョン 4.14 以上
- dune (OCaml のビルドシステム)
- yojson, digestif (OCaml ライブラリ)
- solc (Yulコード(後述)のコンパイルのため)
- npm (出力したバイトコードが動くことのテストのため.ビルド自体には不要)
使い方:
以下のようなディレクトリ構造があるとします.
ocaml2evm/
└─ dir/
├─ contracts/
└─ src/
├─ foo.ml
...
└─ bar.ml
(ディレクトリ名 dir, src は自由ですが contracts は固定です.)
ocaml2evm ディレクトリ下で
$ dune exec ocamyul dir/src/foo.ml
を実行すると foo.ml がコンパイルされ,dir/contracts/(モジュール名).json が出力されます.この JSON ファイルには ABI (Application Binary Interface,コントラクトの関数を呼び出すために必要な情報)とバイトコードが含まれており,これを使うことでコントラクトを Ethereum ネットワーク上にデプロイできます.
たとえば,サンプルコード ocaml2evm/sample/src/erc20.ml のコンパイル結果が ocaml2evm/sample/contracts/ERC20.json です.
サンプルコード
簡単なコントラクトの実装
ocaml2evm/sample/src ディレクトリ下にいくつかサンプルの OCaml コードがあります.
たとえば,ocaml2evm/sample/src/simple_storage.ml は以下です.
module SimpleStorage : sig
type storage
val set : int -> storage -> unit * storage
val get : unit -> storage -> int * storage
val incr : unit -> storage -> unit * storage
val twice : int -> storage -> int * storage
val anormaltest : int -> storage -> int * storage
end = struct
type storage = int
let set n _ = ((), n)
let get () s = (s, s)
let incr () s = ((), s + 1)
let twice n s = (n * 2, s)
let anormaltest n s =
let m =
let l = s * 2 in
let o = n * 3 in
l + o
in
(m, m)
end
ここでは,SimpleStorage というモジュールを作り,その中で set や get などの簡単な関数を定義しています.
まず,SimpleStorage ではストレージ(複数の関数呼び出しで共有されるデータの領域)は整数型の値を持つという意味で,
type storage = int
のように storage 型を定義しています.
各関数は値とストレージを受け取り,値と更新後のストレージのペアを返します.つまり,型は以下の形になっています.
'a -> storage -> 'b * storage
たとえば set 関数の定義
let set n _ = ((), n)
は整数 n を取り,それをストレージに格納し,返り値はなし( = unit 型のコンストラクタ () を返す)ということを意味します.
その他,
-
get関数は「ストレージの値sを取り出して返し,ストレージの変更はなし」, -
incr関数は「ストレージの値をインクリメントして更新する」, -
twice関数は「整数nを取ってその二倍を返し,ストレージの変更はなし」
という定義になっています.
関数定義の中では let ... in 式が使用でき,関数 anormaltest で例を示しています.
anormaltest の定義にあるように,ネストした let ... in 式も扱えます.
前述したように,このファイルは ocaml2evm ディレクトリの中で
$ dune exec ocamyulc sample/src/simple_storage.ml
を実行することでコンパイルでき,その結果が sample/contracts/SimpleStorage.json として出力されます.
JavaScript コード sample/contract_test/SimpleStorage.test.js でこの SimpleStorage コントラクトをローカルネットワーク上でテストしています.
テストは sample ディレクトリ下で
$ npm test
で実行できます.
ERC20 の実装
sample/src/erc20.ml で ERC20 を実装しています.
open OCamYul.Primitives
module ERC20 : sig
type storage
type mut_storage
val total_supply : unit -> storage -> int * storage
val balance_of : address -> storage -> mut_storage -> int * storage
val allowance : address * address -> storage -> mut_storage -> int * storage
val mint : int -> storage -> mut_storage -> unit * storage
val burn : int -> storage -> mut_storage -> unit * storage
val transfer : address * int -> storage -> mut_storage -> unit * storage
val approve : address * int -> storage -> mut_storage -> unit * storage
val transfer_from :
address * address * int -> storage -> mut_storage -> unit * storage
end = struct
type storage = int
type mut_storage =
(address, int) Hashtbl.t * (address, (address, int) Hashtbl.t) Hashtbl.t
let total_supply () total = (total, total)
let balance_of account total (balance, _) =
(Hashtbl.find balance account, total)
let allowance (owner, allowed_address) total (_, allow) =
(Hashtbl.find (Hashtbl.find allow owner) allowed_address, total)
let mint amount total (balance, _) =
let from_address = caller () in
let from_balance = Hashtbl.find balance from_address in
Hashtbl.replace balance from_address (from_balance + amount);
((), total + amount)
let burn amount total (balance, _) =
let from_address = caller () in
let from_balance = Hashtbl.find balance from_address in
Hashtbl.replace balance from_address (from_balance - amount);
((), total - amount)
let transfer (to_address, amount) total (balance, _) =
let from_address = caller () in
let from_balance = Hashtbl.find balance from_address in
let to_balance = Hashtbl.find balance to_address in
Hashtbl.replace balance from_address (from_balance - amount);
Hashtbl.replace balance to_address (to_balance + amount);
((), total)
let approve (allowed_address, amount) total (_, allow) =
Hashtbl.replace (Hashtbl.find allow (caller ())) allowed_address amount;
((), total)
let transfer_from (from_address, to_address, amount) total (balance, allow) =
let from_address_allow = Hashtbl.find allow from_address in
let allowed_balance = Hashtbl.find from_address_allow (caller ()) in
Hashtbl.replace from_address_allow (caller ()) (allowed_balance - amount);
let from_balance = Hashtbl.find balance from_address in
let to_balance = Hashtbl.find balance to_address in
Hashtbl.replace balance from_address (from_balance - amount);
Hashtbl.replace balance to_address (to_balance + amount);
((), total)
end
SimpleStorage と違う点の一つとして,ここでは storage の他に mut_storage という型を定義しています.
storage は整数値のようにイミュータブルな値を格納することを想定しており,一方 mut_storage はハッシュテーブルのようなミュータブルな値を格納するために使います.
(OCaml でいうハッシュテーブルは,Solidity でいう mapping に対応します.)
erc20.ml では mut_storage は二つのハッシュテーブルから成り,一つ目である (address, int) Hashtbl.t は口座のアドレスをキー,その口座の残高を値として持つハッシュテーブルを表します.
二つ目である (address, (address, int) Hashtbl.t) Hashtbl.t は,キー(口座アドレス) a に対して,「それぞれの口座 b について a は b からいくら預金を移してよいかを表すテーブル」を値に持つようなテーブルです.
また,storage はこれまでに発行されたトークン全体の量を表す整数を格納します.
たとえば関数 allowance
let allowance (owner, allowed_address) total (_, allow) =
(Hashtbl.find (Hashtbl.find allow owner) allowed_address, total)
は二つの口座アドレス owner, allowed_address のペアを受け取り,owner が allowed_address の口座から移せるトークンの量を返しています.
また,関数 transfer_from
let transfer_from (from_address, to_address, amount) total (balance, allow) =
let from_address_allow = Hashtbl.find allow from_address in
let allowed_balance = Hashtbl.find from_address_allow (caller ()) in
Hashtbl.replace from_address_allow (caller ()) (allowed_balance - amount);
let from_balance = Hashtbl.find balance from_address in
let to_balance = Hashtbl.find balance to_address in
Hashtbl.replace balance from_address (from_balance - amount);
Hashtbl.replace balance to_address (to_balance + amount);
((), total)
は from_address から to_adress へ amount の量だけトークンを移す,という動作をします.7,8行目の Hashtbl.replace で from_address/to_address の口座残高を amount だけ減らし/増やしています.
また,この移動に伴い4行目で関数の呼び出し元(caller ())が from_address から移してよいトークン量を減らしています.
内部のこと:コンパイルの流れ
コンパイルは,OCaml コードから直接バイトコードを出力しているわけではなく,Yul という中間言語を介しています.
ocaml2evm のメインの部分は OCaml コードを Yul コードに変換しており,生成された Yul コードは Solidity コンパイラである solc の機能を使ってバイトコードに変換しています.
今後など
ocaml2evm コンパイラプロジェクトはまだまだ始動したばかりで,現在は OCaml のごく小さなサブセットしか扱えないのですが,ERC20 を実装できる程度にはなっています.
たとえばまだレコード型や高階関数などは扱えません.
これから扱える機能を増やしていくのはもちろんのこと,今後の展開としては定理証明支援系 Coq を使った検証ができるようにすることも目標の一つです.
ということで,開発中である ocaml2evm を紹介してみました.OCaml と Coq でハッピー・スマートコントラクト・ディヴェロッピングを目指そう!!