SCamlによるTezosプログラミング#2 概観

Tezos ブロックチェーン のためのスマートコントラクト記述言語 SCaml、チュートリアルの第2回目です。

今回はブロックチェーンとTezosの概観です。

このブログではSCamlを使ったTezosスマートコントラクトプログラミングについて少しずつ日本語で連載をしていきたいと思います。

第2回目ですが、まだまだコーディングが始まりません。まずブロックチェーン自体を簡単に説明しないと、先に進めません。面倒な分野です。

(この内容は 2020-03-21 にやった SCaml ハンズオンの資料を手直ししたものです。)

Tezos スマートコントラクト概観

ブロックチェーンとスマートコントラクトが切り開く人類の未来、みたいな話は他でさんざんされているんで、そういうキラキラした部分はすっ飛ばします。

ブロックチェーンとは

ぶっちゃけ分散データベースですわ。夢がなくてすいませんな:

台帳方式
差分を積み上げる (like Git)
公開
誰でもデータベースノードを動かして分散環境に参加できる
インセンティブモデル
合意形成維持のために換金性のあるトークンを持つ
  • データベース使用料 (fee)
  • データベース維持報酬 (reward)

ブロックチェーンが他の分散データベースと違うのは公開されているというところです。誰でもノード(DBインスタンス)を建てれる。そこで open network とか permissionless とか言われる。

分散データベースにはそもそも合意形成不可能という避けられない問題があり、Paxos などの投票による方法で実用化しているのですが、これがオープンネットワークの環境ではアカウントはいくらでも作れてしまうのでうまいきません。それを解決するのが PoW や PoS で、無限にアカウントを作れない仕掛け、かつ正直に行動するインセンティブになっています。もし人類がこれ以外の方法でパブリックデータベースの合意形成問題を解決できれば、そもそも暗号通貨は要らない、とも言えるでしょう。

スマートコントラクトとは

ぶっちゃけ DBトランザクションをトリガーとして実行されるプログラムです:

  • アカウントにコードが紐付け
  • アカウントにトランザクションがあるとコードが起動
  • 実行コードは新たなトランザクションを生成できる

ブロックチェーンとスマートコントラクトの安全性

必要悪とは言え、ブロックチェーンでは換金性のあるトークンを扱うため、バグを突く攻撃をされるとトークンを盗まれてしまいます。実際、巨額な被害額のハッキング事件が複数起きています。

  • 安全なブロックチェーンフレームワークで
  • 安全なスマートコントラクトを開発

しないと危ない(かもしれない)

そこで Tezos! (唐突に)

https://gitlab.com/dailambda/images/-/raw/master/tezos-1.svg?inline=false
  • PoS ブロックチェーン
  • 安全性を重視したデザイン
    • 形式検証技術の積極的な採用

唐突ですがまあいいじゃないですか。

Tezos のスマートコントラクト: Michelson

Tezos のスマートコントラクトは Michelson という仮想機械で実行します。次のような特徴がある。

スタックマシン

Gas モデルによる各オプコードの実行コスト計算

副作用がない(純粋関数型)

副作用はプログラムの挙動の理解を難しくする。

Michelson は副作用を排除し、副作用による思わぬバグを回避。

強い型付け

暗黙のキャストによる恐ろしいバグを回避。JavaScript での例:

// JavaScript
let x = 10;
let y = '10';
let z = x + y  // '1010' 😮
let w = z - 1  // 1009

Michelson ではこう言ったことが起こらないよう型システムでがっちりプログラムを守っている。

高レベルデータ構造をVMレベルでサポート

複雑なデータ構造を低レベル言語で実装するのは難しい。Parity事件では、そのようなライブラリにバグがあり、それを利用され32億円が盗まれました。

TezosはVMレベルでいくつかの複雑なデータ型をネイティブにサポートしています:

  • 多倍長整数。オーバーフローは起こらない。
  • Pair((e1,e2))、option(None/ Some e), sum(Left e1 / Right e2)
  • List, set(集合), map(連想配列)

もし、これらにないデータ型がどうしても必要な場合は、コミュニティで議論の上、プロトコルアップデートで付け加えていくことができます。

暗黙のエラーの排除

エラーが発生しうる箇所は option 型を使ってあるのでエラー処理を忘れることがありません。例えば除算のオペレータ DIV の挙動は(二項演算子的に書けば)こうなっています:

  • 4 DIV 2Some 2
  • 4 DIV 0None

0 での割り算は Divided by zero エラーにするのではなく、オプション型を使い None を返すようにしています。

極少数の命令では例外が発生することがあります。Michelson では例外はキャッチできません。例外はコントラクトの実行全体を即中断します。この場合ブロックチェーン状態は変わらず、明示的な状態巻き戻しの必要がありません。

コントラクト呼び出し ≠ 関数コール

コントラクト間関数呼び出しは便利だが、プログラムの安全性解析を難しくします。DAO事件では、コントラクト間関数呼び出し周辺のバグ(reentrancy bug)を突かれ、一度しか起こらないはずの支払いを繰り返し発生させる手法で50億円の窃盗が行われました。

https://gitlab.com/dailambda/images/-/raw/master/reentrancy-bug.svg?inline=false

Tezos では思い切ってコントラクト間関数呼び出しはできなくなっています:

  • コントラクトから別のコントラクトにトランザクションを送ることはできる。だが、その結果を元のコントラクトが得ることはできません。
  • 関数呼び出しに例えるとコントラクト呼び出しは「返り値のない関数」のようなものになります。

これは Ethereum のようなコントラクト間関数呼び出しがごく普通に使われているスマートコントラクト世界のプログラミングとモデルが大きく異なるので戸惑うかもしれませんが、一つのプログラムを複数のコントラクトに切り刻まない、継続呼び出しスタイル(CPS)を使うなどすれば問題ありません。(このチュートリアルでは扱いません。)

注意: 現在の Michelson には、 view と呼ばれる他のコントラクトの関数を呼び出すしくみがあります。ただし、view は純粋関数でなければなりません。Reentrancy bug を防ぐため、呼び出し先のコントラクトの状態を変更することはできません。

ただ… Michelson は.. むずかしい

典型的な Michelson プログラムの例。スタックマシンの命令列。訓練しても読むのに時間がかかります:

parameter
  (or
     (lambda %do unit (list operation))
     (unit %default));
storage key_hash;
code
  { UNPAIR ;
    IF_LEFT
      { # 'do' entrypoint
        # Assert no token was sent:
        # to send tokens, the default entry point should be used
        PUSH mutez 0 ;
        AMOUNT ;
        ASSERT_CMPEQ ;
        # Assert that the sender is the manager
        DUUP ;
        IMPLICIT_ACCOUNT ;
        ADDRESS ;
        SENDER ;
        ASSERT_CMPEQ ;
        # Execute the lambda argument
        UNIT ;
        EXEC ;
        PAIR ;
      }
      { # 'default' entrypoint
        DROP ;
        NIL operation ;
        PAIR ;
      }
  };

(ただし、Michelson は型があるので、普通のスタックマシンのプログラムに比べれば驚くほど書きやすくはあります。)

高級言語から Michelson へのコンパイラ

Michelson を手書きするのはあまりに参入障壁が高いので、いくつか高級言語から Michelson に変換するコンパイラが提案されています:

例えば、次は先程の Michelson コードと同等の SCaml プログラム。理解する必要はないが、まだ読めそう、ではなかろうか:

open SCaml

let [@entry] do_ (f : unit -> operations) kh =
  if Global.get_amount () <> Tz 0. then failwith "non 0 amount";
  if Global.get_sender () = Contract.address (Contract.implicit_account kh) then 
    failwith "permission denied";
  f (), kh

let [@entry] default () kh =
   [], kh

というわけで、SCaml をやっていくよ

https://gitlab.com/dailambda/images/-/raw/master/scaml-page.png?inline=false

なぜ SCaml か: 既存のプログラミング言語 OCaml の厳密なサブセット

  • OCaml はいい言語
  • OCaml を知っていればすぐに使える
  • OCaml と同じ高性能なフロントエンド
  • OCaml のツールがそのまま使える(ハイライター、インデンター)
  • OCaml の本が使える(Real World OCaml, プログラミング in OCaml)
  • SCaml 勉強したら OCaml も勉強できる
  • 自分で作ったので中身わかっているw