株式会社 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 でハッピー・スマートコントラクト・ディヴェロッピングを目指そう!!