Tezos でスマートコントラクトを作る 1

以前の記事で Tezos の説明を書いた際に、スマートコントラクトについて言及した。

https://qiita.com/cedretaber/items/264c6fdf4494b0fdd4e4

このスマートコントラクトという機能が Tezos 等の暗号通貨の非常に大きな特徴だと思うので、是非ともこれを触ってみたい。
なので、この記事では Tezos でスマートコントラクトを取り扱ってみたいと思う。

## Tezos とスマートコントラクト

スマートコントラクトというのは、暗号通貨のネットワーク上に登録されて実行されるプログラムで、どういう条件で何が実行されるのか、誰もが見ることができるという特徴がある。
1つのスマートコントラクトが1つのアカウントみたいに扱われ、スマートコントラクトに対して Tezos の送金を行う事ができる。どういうコードが実行されるか読むことができるので、全て納得した上で送金する事ができるというわけだ。スマートコントラクトのプログラムに従って、スマートコントラクトから他のアカウントに送金したりもできるらしい。
以前も書いたけど、イーサリアムとかにもこの機能はあるし、この機能を使ってイーサリアム上に他の暗号通貨を構築するなどで使われているらしい。

で、 Tezos のスマートコントラクトの特徴なのだけど、スマートコントラクトを記述するのに Michelson という言語（かつ、その言語が実行される VM の名前でもある）を用いる点にある。これは厳格な静的型を持つスタックベースの言語なのだけど、「静的型付き」と強調した通り、型検査によりバグの混入を抑えられるというわけだ。
この辺りの特徴は、流石に作ってる人達が関数型勢なだけはあるなぁと感じるところ。

## スマートコントラクトとプログラミング言語

それで、じゃあスマートコントラクトを作成するのにその Michelson を書くかという話なのだが、流石にスタックベースの言語は独特で、直接記述するのは辛いものがある。
Tezos 開発側ももちろんそれを把握しているので、イーサリアムの Solidity みたいに、 Michelson にコンパイルされる高級言語を公式で用意している。 LIGO という言語がそれ。

https://ligolang.org/

ちょっと面白い言語で、なんと表層構文が4種類（！？）も用意されている。 JS 風、 OCaml 風、 Pascal 風、 Reason 風らしい。
わざわざ Pascal 風が用意してあったり、 JS 風と Reason 風が両方とも用意してあったりと、かなり開発者の癖が出ている興味深いチョイスだと思う。
ただこの「風」というのが曲者で、結局の所その元の言語ではないので、ライブラリなどの資源が流用できなかったり、コーディングの上でも無数の落とし穴が自ずと生まれたりして、中々思い通りに使えないものだったりする。

LIGO の他にも、 Python で開発できる SmartPy というやつもあるらしい。

https://smartpy.io/

要するに Python コードを Michelson に変換してくれるそうなのだが、公式サイトによると、まず Python を OCaml ベースの SmartML というのに変換してから、それを Michelson に変換するらしい。
単なる言語ではなくて、 Python でスマートコントラクトを開発するためのツールセットで、テストスイートや IDE までも含んでいると書いてある。なるほどとても便利そうで、特にテスト環境が整っている点は非常に魅力的なのだけど、 Python を書かないといけないのが何だかなぁという気分にはなる。 Michelson は静的型付き言語なのに。

どうせ途中で OCaml に変換するのなら、OCaml を直接書かせてくれれば良いのに……。
というわけで、そういう言語を使っていくことにする。

## SCaml

SCaml もまた OCaml ベースの言語で Michelson にコンパイルされるのだが、特徴として OCaml の完全なサブセットである点が挙げられる。
つまり、上に書いた「○○風」の落とし穴が存在せず、かつ OCaml というよく出来た静的型付き言語の機能を縦横に利用できるわけだ。
これだよ、求めていたものは！　という感じである。

https://gitlab.com/dailambda/scaml/-/tree/master

開発しているのはダイラムダ株式会社の古瀬さん。 Tezos のコア開発にも参画されている。
公式ブログで SCaml の紹介とチュートリアルの記事も公開されている。

https://www.dailambda.jp/blog/2020-02-01-scaml-jp-0/

このチュートリアルがとても分かりやすいので、この記事通りに進めていきたいと思う。

## Tezos の準備を行う

以前の記事で行ったのと同じように、 Tezos を使う為の準備を行う。
前は opam を利用してコンパイルしたけど、今回はバイナリを落としてきてそれを使ってみようかと思う。どうせテストネットにしか接続しないつもりなので。

https://tezos.gitlab.io/introduction/howtoget.html#getting-static-binaries

普通にバイナリを落としてきて、権限を変えてからバージョンを確認。

```bash
$ ./tezos-client --version
4ca33194 (2022-08-01 11:55:43 +0200) (14.0)

$ ./tezos-node --version
4ca33194 (2022-08-01 11:55:43 +0200) (14.0)
```

ヨシ！

次にテストネットに繋いでいく。

https://tezos.gitlab.io/introduction/test_networks.html

テストネットは、バージョンが固定されたものには現行のプロトコルと最新のプロトコルとの2つが提供されているのだが、それとは別にバージョンが固定されない Ghostnet というものがある。
これは長期運用の為のテストネットで、メインネットと同じようにプロトコルがアップデートされていく。プロトコルが固定されたテストネットだと、各プロトコル固有のテストはできても、所謂ステージング環境のような長く利用する運用が行い難いので（プロトコルのバージョンが切り替わる度に環境がリセットされてしまうので）、そういう場合はこちらを利用すると良さそうだ。

今回はその ghostnet を使ってみる。

```bash
$ ./tezos-node config init --data-dir ./.tezos-node-ghostnet --network ghostnet
Aug 24 03:15:38.173 - node.config.validation: the node configuration has been successfully validated.
Created ./.tezos-node-ghostnet/config.json for network: ghostnet.

$ ./tezos-node identity generate --data-dir ./.tezos-node-ghostnet
Generating a new identity... (level: 26.00)
Stored the new identity (idtw1wcubuCe1p4s5LewrADTCB2WS6) into './.tezos-node-ghostnet/identity.json'.
```

良さそうである。
全部の取引データを持ってきていてはいつまでかかるか分からないので、以前やったようにスナップショットを利用する。

https://ghostnet.xtz-shots.io/

```bash
$ wget https://ghostnet.xtz-shots.io/ghostnet-1043996.rolling

$ ./tezos-node snapshot import ghostnet-1043996.rolling --block BM7mXRYL1zD7KxbzupzYmFvzYeXVrxRvszxp1J7k58R5oqhBLao --data-dir ./.tezos-node-ghostnet
Aug 24 03:20:53.785 - node.snapshots: importing data from snapshot
Aug 24 03:20:53.785 - node.snapshots:   ghostnet-1043996.rolling: chain TEZOS_ITHACANET_2022-01-25T15:00:00Z, block hash BM7mXRYL1zD7KxbzupzYmFvzYeXVrxRvszxp1J7k58R5oqhBLao at level 1043996, timestamp 2022-08-20T01:22:35-00:00 in rolling (snapshot version 4)
Aug 24 03:20:53.785 - node.snapshots: retrieving and validating data. This can take a while, please bear with us
Writing context: 1786K/1786K (100%) elements, 369MiB read Done
Copying protocols: 1/1 Done
Storing floating blocks: 120 blocks written Done
Aug 24 03:21:23.256 - node.snapshots: successful import from file ghostnet-1043996.rolling
```

読み込めた。
ではノードを立ち上げてみよう。

```bash
$ ./tezos-node run --data-dir ./.tezos-node-ghostnet --rpc-addr 127.0.0.1
```

クライアント側でも準備をする。

```bash
$ ./tezos-client bootstrapped
```

スナップショットを使ってすら、そこそこ時間がかかる。
timestamp が徐々に現時点に近付いてくるのを楽しみながら、ゆっくりと待つ。
`Node is bootstrapped.` と言われれば準備完了である。

さて、準備が終わったら無料の Tezos を受け取っておこう。テストネットでは大量の Tezos を無料でもらう事ができる。残念なのは換金が不可能な点のみである。

https://teztnets.xyz/ghostnet-faucet

```bash
$ ./tezos-client activate account faucet with ./ghostnet.json

$ ./tezos-client get balance for faucet
Warning:

                 This is NOT the Tezos Mainnet.

           Do NOT use your fundraiser keys on this network.

76461.899059 ꜩ
```

良さそう。
（この「今使ってるのはメインネットじゃないから気をつけろよ」という警告は毎回出てくるので、以下省略する。）

## SCaml の準備を行う

基本的にダイラムダさんのブログに書いてある手順をこなしていくだけである。

https://www.dailambda.jp/blog/2020-05-13-scaml-jp-1/

```bash
$ git clone https://gitlab.com/dailambda/docker-tezos-hands-on

$ cd docker-tezos-hands-on

$ ./check
```

これでエラーが出なければ良さそうである。

では、最小のプログラムを書いてコンパイルしてみる。

```ocaml:simplest.ml
open SCaml

let [@entry] main () () = [], ()
```

```bash
$ ./scamlc simplest.ml
Linking Simplest
Compiling simplest Simplest_0
```

コンパイルできた！
simplest.tz というファイルに吐き出されている。

```bash
$ cat simplest.tz
parameter unit ;
storage unit ;
code { DROP # let param_storage_164 = (input, storage)
            # free param_storage_164
     ; UNIT # let _unit_181 = ()
     ; NIL operation # let _ops_182 = []
     ; PAIR # let _pair_183 = (_ops_182, _unit_181)
            # return _pair_183
     }
```

Michelson にコンパイルされている！

## テストネットにデプロイする

この最小のスマートコントラクトを、いよいよテストネットに登録してみよう。
これもダイラムダさんのブログ記事通りに。

```bash
./tezos-client originate contract simplest \
    transferring 0 from faucet \
    running docker-tezos-hands-on/simplest.tz \
    --burn-cap 100
Node is bootstrapped.
Estimated storage: no bytes added
Estimated gas: 1408.922 units (will add 100 for safety)
Estimated storage: 297 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'onhryTUhoZXwASV2VnzVmJSJtw2jUMBzLWNDZDDEnUSaG6RGQhJ'
Waiting for the operation to be included...
Operation found in block: BMKhWjgPPQhCoR6JriyjKXfgX5NpSjzE2Rg5p11Hwqaz7skj4Wt (pass: 3, offset: 0)
This sequence of operations was run:
  Manager signed operations:
    From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
    Fee to the baker: ꜩ0.00036
    Expected counter: 11693406
    Gas limit: 1000
    Storage limit: 0 bytes
    Balance updates:
      tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.00036
      payload fees(the block proposer) ....... +ꜩ0.00036
    Revelation of manager public key:
      Contract: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
      Key: edpkuNWJjP8jW56BDKSrMhiB5bJraJpUpu59ux4KgJketjrgnv7nzF
      This revelation was successfully applied
      Consumed gas: 1000
  Manager signed operations:
    From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
    Fee to the baker: ꜩ0.000325
    Expected counter: 11693407
    Gas limit: 1509
    Storage limit: 317 bytes
    Balance updates:
      tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000325
      payload fees(the block proposer) ....... +ꜩ0.000325
    Origination:
      From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
      Credit: ꜩ0
      Script:
        { parameter unit ;
          storage unit ;
          code { DROP ; UNIT ; NIL operation ; PAIR } }
        Initial storage: Unit
        No delegate for this contract
        This origination was successfully applied
        Originated contracts:
          KT1MuUCgjWF1wMBGBVsLQE3prKV3Tyu6vwFW
        Storage size: 40 bytes
        Paid storage size diff: 40 bytes
        Consumed gas: 1408.922
        Balance updates:
          tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.01
          storage fees ........................... +ꜩ0.01
          tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.06425
          storage fees ........................... +ꜩ0.06425

New contract KT1MuUCgjWF1wMBGBVsLQE3prKV3Tyu6vwFW originated.
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
  tezos-client wait for onhryTUhoZXwASV2VnzVmJSJtw2jUMBzLWNDZDDEnUSaG6RGQhJ to be included --confirmations 1 --branch BLyhvn8r3NnUVpPvBi5ZC9gWCG5qkmg5WxC7zmw8NZra1hNXvK5
and/or an external block explorer.
Contract memorized as simplest.
```

登録できたようだ。
呼び出してみよう。

```bash
$ ./tezos-client transfer 0 from faucet to simplest
Node is bootstrapped.
Estimated gas: 2110.410 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'ooDSpJ7trNCPvBLV1UGpVwEeDD2nhkJmC46V5oqsTvViTC3GKAn'
Waiting for the operation to be included...
Operation found in block: BMUmSRo48coPJ61zv8irhvWnebBTTDGtzQKTniPtmTzgqzN8Z7w (pass: 3, offset: 1)
This sequence of operations was run:
  Manager signed operations:
    From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
    Fee to the baker: ꜩ0.000473
    Expected counter: 11693408
    Gas limit: 2211
    Storage limit: 0 bytes
    Balance updates:
      tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000473
      payload fees(the block proposer) ....... +ꜩ0.000473
    Transaction:
      Amount: ꜩ0
      From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
      To: KT1MuUCgjWF1wMBGBVsLQE3prKV3Tyu6vwFW
      This transaction was successfully applied
      Updated storage: Unit
      Storage size: 40 bytes
      Consumed gas: 2110.410
```

期待通りの挙動（何もしない）をしている。
これで、自作のスマートコントラクトがネットワークに登録できたわけだ。

## もう少し多機能なスマートコントラクトを書いてみる

これだと余りにも機能が少なくて寂しいので、もう少し機能のあるスマートコントラクトを作ってみよう。
上述した SmartPy のサイトに載っているサンプルを SCaml で実装してみたいと思う。
このスマートコントラクトが持つ機能はこうだ。

- ストレージには数値を1つ持つ
- replace エントリーポイントは、ストレージを与えられた数値で置き換える
- double エントリーポイントは、ストレージの数値を2倍にする

SmartPy のコードではこのようになっている。

```python
import smartpy as sp

class StoreValue(sp.Contract):
  def __init__(self, value):
      self.init(storedValue = value)

  @sp.entry_point
  def replace(self, value):
      self.data.storedValue = value

  @sp.entry_point
  def double(self):
      self.data.storedValue *= 2
```

では作ってみよう。
まず SCaml のコードを書く。

```ocaml:example.ml
open SCaml

let [@entry] replace value _current = [], value

let [@entry] double () current = [], (current * Int 2)
```

複雑性が増したと言っても、機能は僅か1行である。
SCaml の詳しい説明はダイラムダさんのブログ記事に譲るが、コードの見所は次のような感じ。

- エントリーポイントの関数は引数を2つ取り、最初の引数が呼び出し時に与えられたパラメータ、次の引数がストレージの中身
- エントリーポイントの関数が返すのは「オペレーション」と「ストレージ」のタプル
  - この時、新しいストレージを返すことでストレージを更新することができる
- Michelson  で利用される数値は整数、自然数、トークンの3種類なので、計算を行う場合は型を合わせなければいけない
  - 逆に言えば、「トークンに整数を加算」するような誤ったコードはコンパイル時に弾かれる

このコードを Michelson にコンパイルする。

```bash
$ ./scamlc example.ml
```

吐き出された Michelson ファイルを、 SmartPy のサイトに載っている Michelson コードと見比べてみると面白い。
コンパイラが違うので、全く同じコードにはなっていないが、結果的には同じ機能を持つはずだ。

ではこの Michelson コードをテストネットにデプロイする。

```bash
./tezos-client originate contract example \
    transferring 0 from faucet \
    running docker-tezos-hands-on/example.tz \
    --init '0' \
    --burn-cap 100
Node is bootstrapped.
Estimated gas: 1421.297 units (will add 100 for safety)
Estimated storage: 356 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'oofJqpkA7X4YK8dDbXGdN9Y8GxRNYFS1EZH7sM8SdGeDQ3pLsAq'
Waiting for the operation to be included...
Operation found in block: BLmnszL4LCKWVeHxfBwvvGaNhciXDNeEieiog9E9MebJRc3d1Gv (pass: 3, offset: 0)
This sequence of operations was run:
  Manager signed operations:
    From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
    Fee to the baker: ꜩ0.000482
    Expected counter: 11693409
    Gas limit: 1522
    Storage limit: 376 bytes
    Balance updates:
      tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000482
      payload fees(the block proposer) ....... +ꜩ0.000482
    Origination:
      From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
      Credit: ꜩ0
      Script:
        { parameter (or (int %replace) (unit %double)) ;
          storage int ;
          code { UNPAIR ;
                 IF_LEFT
                   { SWAP ; DROP ; NIL operation ; PAIR }
                   { DROP ; PUSH int 2 ; SWAP ; MUL ; NIL operation ; PAIR } } }
        Initial storage: 0
        No delegate for this contract
        This origination was successfully applied
        Originated contracts:
          KT1Uy3VGP9uWKfY8FR1NagDWhaCCnBe9gVf7
        Storage size: 99 bytes
        Paid storage size diff: 99 bytes
        Consumed gas: 1421.297
        Balance updates:
          tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.02475
          storage fees ........................... +ꜩ0.02475
          tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.06425
          storage fees ........................... +ꜩ0.06425

New contract KT1Uy3VGP9uWKfY8FR1NagDWhaCCnBe9gVf7 originated.
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
  tezos-client wait for oofJqpkA7X4YK8dDbXGdN9Y8GxRNYFS1EZH7sM8SdGeDQ3pLsAq to be included --confirmations 1 --branch BM82aPJLxBtZvjtaJJT3Tg1KTmjR39CSdXYPBXaBqyQQ8BXRQ44
and/or an external block explorer.
Contract memorized as example.
```

宜しい。
`Initial storage: 0` なので、ストレージが `0` で初期化されていることが分かるだろう。
よく見ると表示されている Michelson コードが渡したものとちょっと違うが、コメントを取り除いたりするだけじゃなく簡単な最適化も行ってくれているのだろうか。

それでは、呼び出してみよう。
複数のエントリーポイントを持つスマートコントラクトの場合、 `--entrypoint` でどのエントリーポイントを呼び出すのかを指定する。

```bash
$ ./tezos-client transfer 0 from faucet to example --entrypoint replace --arg 42 --burn-cap 100
Node is bootstrapped.
Estimated gas: 2119.147 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'op5yNxoRrgAyMbk6VBajdZpymyhtvupvWmWTbDbGUqUdHW7KCwC'
Waiting for the operation to be included...
Operation found in block: BMeYHtMavEBVXWaThikoThkBzv7mySMpzdTFxYbDuhyt2B1DoZn (pass: 3, offset: 0)
This sequence of operations was run:
  Manager signed operations:
    From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
    Fee to the baker: ꜩ0.000488
    Expected counter: 11693410
    Gas limit: 2220
    Storage limit: 0 bytes
    Balance updates:
      tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000488
      payload fees(the block proposer) ....... +ꜩ0.000488
    Transaction:
      Amount: ꜩ0
      From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
      To: KT1Uy3VGP9uWKfY8FR1NagDWhaCCnBe9gVf7
      Entrypoint: replace
      Parameter: 42
      This transaction was successfully applied
      Updated storage: 42
      Storage size: 99 bytes
      Consumed gas: 2119.147
```

処理が成功し、ストレージが渡した引数である `42` で置き換えられた事が伝えられている。

では、もう片方のエントリーポイントも呼び出してみよう。
こちらは引数不要だ。

```bash
$ ./tezos-client transfer 0 from faucet to example --entrypoint double --burn-cap 100
Node is bootstrapped.
Estimated gas: 1203.625 units (will add 100 for safety)
Estimated storage: 1 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'opPCwkLRp7RgUTDa38Wca71nk7Cw1vFLL161aB93tEdfhAxT3fA'
Waiting for the operation to be included...
Operation found in block: BMUbjipj2cbaNfnKJ7PcEZRQSZ2QFrgWmCbmrkXxdnVGyr8ncgS (pass: 3, offset: 0)
This sequence of operations was run:
  Manager signed operations:
    From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
    Fee to the baker: ꜩ0.000396
    Expected counter: 11693411
    Gas limit: 1304
    Storage limit: 21 bytes
    Balance updates:
      tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000396
      payload fees(the block proposer) ....... +ꜩ0.000396
    Transaction:
      Amount: ꜩ0
      From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
      To: KT1Uy3VGP9uWKfY8FR1NagDWhaCCnBe9gVf7
      Entrypoint: double
      This transaction was successfully applied
      Updated storage: 84
      Storage size: 100 bytes
      Paid storage size diff: 1 bytes
      Consumed gas: 1203.625
      Balance updates:
        tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.00025
        storage fees ........................... +ꜩ0.00025
```

同様に成功して、ストレージが `42` の2倍の `84` で置き換えられている。

こんな感じで、多少の機能を持ったスマートコントラクトでも簡単に開発、デプロイ、呼び出しができる。

## まとめ

Tezos のスマートコントラクトを記述し、デプロイすることができた。
思ったより簡単だったな、というのがやってみた感想かな。
とはいえ今回作ったスマートコントラクトはどれも非常に単純な、遊びのようなものなので、もう少し面白いスマートコントラクトを作ってみたいところ。